// Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. // Flags: --expose-internals 'use strict'; const common = require('../common'); common.skipIfDumbTerminal(); const assert = require('assert'); const readline = require('readline'); const util = require('util'); const { getStringWidth, stripVTControlCharacters } = require('internal/util/inspect'); const { EventEmitter, getEventListeners } = require('events'); const { Writable, Readable } = require('stream'); class FakeInput extends EventEmitter { resume() {} pause() {} write() {} end() {} } function isWarned(emitter) { for (const name in emitter) { const listeners = emitter[name]; if (listeners?.warned) return true; } return false; } function getInterface(options) { const fi = new FakeInput(); const rli = new readline.Interface({ input: fi, output: fi, ...options, }); return [rli, fi]; } function assertCursorRowsAndCols(rli, rows, cols) { const cursorPos = rli.getCursorPos(); assert.strictEqual(cursorPos.rows, rows); assert.strictEqual(cursorPos.cols, cols); } { const input = new FakeInput(); const rl = readline.Interface({ input }); assert(rl instanceof readline.Interface); } { const fi = new FakeInput(); const rli = new readline.Interface( fi, fi, common.mustCall((line) => [[], line]), true, ); assert(rli instanceof readline.Interface); fi.emit('data', 'a\t'); rli.close(); } [ undefined, 50, 0, 100.5, 5000, ].forEach((crlfDelay) => { const [rli] = getInterface({ crlfDelay }); assert.strictEqual(rli.crlfDelay, Math.max(crlfDelay || 100, 100)); rli.close(); }); { const input = new FakeInput(); // Constructor throws if completer is not a function or undefined ['not an array', 123, 123n, {}, true, Symbol(), null].forEach((invalid) => { assert.throws(() => { readline.createInterface({ input, completer: invalid }); }, { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE' }); }); // Constructor throws if history is not an array ['not an array', 123, 123n, {}, true, Symbol(), null].forEach((history) => { assert.throws(() => { readline.createInterface({ input, history, }); }, { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }); }); // Constructor throws if historySize is not a positive number [-1, NaN].forEach((historySize) => { assert.throws(() => { readline.createInterface({ input, historySize, }); }, { name: 'RangeError', code: 'ERR_OUT_OF_RANGE', }); }); // Constructor throws if type of historySize is not a number ['not a number', {}, true, Symbol(), null].forEach((historySize) => { assert.throws(() => { readline.createInterface({ input, historySize, }); }, { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE', }); }); // Check for invalid tab sizes. assert.throws( () => new readline.Interface({ input, tabSize: 0 }), { code: 'ERR_OUT_OF_RANGE' } ); assert.throws( () => new readline.Interface({ input, tabSize: '4' }), { code: 'ERR_INVALID_ARG_TYPE' } ); assert.throws( () => new readline.Interface({ input, tabSize: 4.5 }), { code: 'ERR_OUT_OF_RANGE', message: 'The value of "tabSize" is out of range. ' + 'It must be an integer. Received 4.5' } ); } // Sending a single character with no newline { const fi = new FakeInput(); const rli = new readline.Interface(fi, {}); rli.on('line', common.mustNotCall()); fi.emit('data', 'a'); rli.close(); } // Sending multiple newlines at once that does not end with a new line and a // `end` event(last line is). \r should behave like \n when alone. { const [rli, fi] = getInterface({ terminal: true }); const expectedLines = ['foo', 'bar', 'baz', 'bat']; rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLines.shift()); }, expectedLines.length - 1)); fi.emit('data', expectedLines.join('\r')); rli.close(); } // \r at start of input should output blank line { const [rli, fi] = getInterface({ terminal: true }); const expectedLines = ['', 'foo' ]; rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLines.shift()); }, expectedLines.length)); fi.emit('data', '\rfoo\r'); rli.close(); } // \t does not become part of the input when there is a completer function { const completer = (line) => [[], line]; const [rli, fi] = getInterface({ terminal: true, completer }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'foo'); })); for (const character of '\tfo\to\t') { fi.emit('data', character); } fi.emit('data', '\n'); rli.close(); } // \t when there is no completer function should behave like an ordinary // character { const [rli, fi] = getInterface({ terminal: true }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, '\t'); })); fi.emit('data', '\t'); fi.emit('data', '\n'); rli.close(); } // Adding history lines should emit the history event with // the history array { const [rli, fi] = getInterface({ terminal: true }); const expectedLines = ['foo', 'bar', 'baz', 'bat']; rli.on('history', common.mustCall((history) => { const expectedHistory = expectedLines.slice(0, history.length).reverse(); assert.deepStrictEqual(history, expectedHistory); }, expectedLines.length)); for (const line of expectedLines) { fi.emit('data', `${line}\n`); } rli.close(); } // Altering the history array in the listener should not alter // the line being processed { const [rli, fi] = getInterface({ terminal: true }); const expectedLine = 'foo'; rli.on('history', common.mustCall((history) => { assert.strictEqual(history[0], expectedLine); history.shift(); })); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLine); assert.strictEqual(rli.history.length, 0); })); fi.emit('data', `${expectedLine}\n`); rli.close(); } // Duplicate lines are removed from history when // `options.removeHistoryDuplicates` is `true` { const [rli, fi] = getInterface({ terminal: true, removeHistoryDuplicates: true }); const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat']; // ['foo', 'baz', 'bar', bat']; let callCount = 0; rli.on('line', (line) => { assert.strictEqual(line, expectedLines[callCount]); callCount++; }); fi.emit('data', `${expectedLines.join('\n')}\n`); assert.strictEqual(callCount, expectedLines.length); fi.emit('keypress', '.', { name: 'up' }); // 'bat' assert.strictEqual(rli.line, expectedLines[--callCount]); fi.emit('keypress', '.', { name: 'up' }); // 'bar' assert.notStrictEqual(rli.line, expectedLines[--callCount]); assert.strictEqual(rli.line, expectedLines[--callCount]); fi.emit('keypress', '.', { name: 'up' }); // 'baz' assert.strictEqual(rli.line, expectedLines[--callCount]); fi.emit('keypress', '.', { name: 'up' }); // 'foo' assert.notStrictEqual(rli.line, expectedLines[--callCount]); assert.strictEqual(rli.line, expectedLines[--callCount]); assert.strictEqual(callCount, 0); fi.emit('keypress', '.', { name: 'down' }); // 'baz' assert.strictEqual(rli.line, 'baz'); assert.strictEqual(rli.historyIndex, 2); fi.emit('keypress', '.', { name: 'n', ctrl: true }); // 'bar' assert.strictEqual(rli.line, 'bar'); assert.strictEqual(rli.historyIndex, 1); fi.emit('keypress', '.', { name: 'n', ctrl: true }); assert.strictEqual(rli.line, 'bat'); assert.strictEqual(rli.historyIndex, 0); // Activate the substring history search. fi.emit('keypress', '.', { name: 'down' }); // 'bat' assert.strictEqual(rli.line, 'bat'); assert.strictEqual(rli.historyIndex, -1); // Deactivate substring history search. fi.emit('keypress', '.', { name: 'backspace' }); // 'ba' assert.strictEqual(rli.historyIndex, -1); assert.strictEqual(rli.line, 'ba'); // Activate the substring history search. fi.emit('keypress', '.', { name: 'down' }); // 'ba' assert.strictEqual(rli.historyIndex, -1); assert.strictEqual(rli.line, 'ba'); fi.emit('keypress', '.', { name: 'down' }); // 'ba' assert.strictEqual(rli.historyIndex, -1); assert.strictEqual(rli.line, 'ba'); fi.emit('keypress', '.', { name: 'up' }); // 'bat' assert.strictEqual(rli.historyIndex, 0); assert.strictEqual(rli.line, 'bat'); fi.emit('keypress', '.', { name: 'up' }); // 'bar' assert.strictEqual(rli.historyIndex, 1); assert.strictEqual(rli.line, 'bar'); fi.emit('keypress', '.', { name: 'up' }); // 'baz' assert.strictEqual(rli.historyIndex, 2); assert.strictEqual(rli.line, 'baz'); fi.emit('keypress', '.', { name: 'up' }); // 'ba' assert.strictEqual(rli.historyIndex, 4); assert.strictEqual(rli.line, 'ba'); fi.emit('keypress', '.', { name: 'up' }); // 'ba' assert.strictEqual(rli.historyIndex, 4); assert.strictEqual(rli.line, 'ba'); // Deactivate substring history search and reset history index. fi.emit('keypress', '.', { name: 'right' }); // 'ba' assert.strictEqual(rli.historyIndex, -1); assert.strictEqual(rli.line, 'ba'); // Substring history search activated. fi.emit('keypress', '.', { name: 'up' }); // 'ba' assert.strictEqual(rli.historyIndex, 0); assert.strictEqual(rli.line, 'bat'); rli.close(); } // Duplicate lines are not removed from history when // `options.removeHistoryDuplicates` is `false` { const [rli, fi] = getInterface({ terminal: true, removeHistoryDuplicates: false }); const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat']; let callCount = 0; rli.on('line', (line) => { assert.strictEqual(line, expectedLines[callCount]); callCount++; }); fi.emit('data', `${expectedLines.join('\n')}\n`); assert.strictEqual(callCount, expectedLines.length); fi.emit('keypress', '.', { name: 'up' }); // 'bat' assert.strictEqual(rli.line, expectedLines[--callCount]); fi.emit('keypress', '.', { name: 'up' }); // 'bar' assert.notStrictEqual(rli.line, expectedLines[--callCount]); assert.strictEqual(rli.line, expectedLines[--callCount]); fi.emit('keypress', '.', { name: 'up' }); // 'baz' assert.strictEqual(rli.line, expectedLines[--callCount]); fi.emit('keypress', '.', { name: 'up' }); // 'bar' assert.strictEqual(rli.line, expectedLines[--callCount]); fi.emit('keypress', '.', { name: 'up' }); // 'foo' assert.strictEqual(rli.line, expectedLines[--callCount]); assert.strictEqual(callCount, 0); rli.close(); } // Regression test for repl freeze, #1968: // check that nothing fails if 'keypress' event throws. { const [rli, fi] = getInterface({ terminal: true }); const keys = []; const err = new Error('bad thing happened'); fi.on('keypress', (key) => { keys.push(key); if (key === 'X') { throw err; } }); assert.throws( () => fi.emit('data', 'fooX'), (e) => { assert.strictEqual(e, err); return true; } ); fi.emit('data', 'bar'); assert.strictEqual(keys.join(''), 'fooXbar'); rli.close(); } // History is bound { const [rli, fi] = getInterface({ terminal: true, historySize: 2 }); const lines = ['line 1', 'line 2', 'line 3']; fi.emit('data', lines.join('\n') + '\n'); assert.strictEqual(rli.history.length, 2); assert.strictEqual(rli.history[0], 'line 3'); assert.strictEqual(rli.history[1], 'line 2'); } // Question { const [rli] = getInterface({ terminal: true }); const expectedLines = ['foo']; rli.question(expectedLines[0], () => rli.close()); assertCursorRowsAndCols(rli, 0, expectedLines[0].length); rli.close(); } // Sending a multi-line question { const [rli] = getInterface({ terminal: true }); const expectedLines = ['foo', 'bar']; rli.question(expectedLines.join('\n'), () => rli.close()); assertCursorRowsAndCols( rli, expectedLines.length - 1, expectedLines.slice(-1)[0].length); rli.close(); } { // Beginning and end of line const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); fi.emit('keypress', '.', { ctrl: true, name: 'a' }); assertCursorRowsAndCols(rli, 0, 0); fi.emit('keypress', '.', { ctrl: true, name: 'e' }); assertCursorRowsAndCols(rli, 0, 19); rli.close(); } { // Back and Forward one character const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); assertCursorRowsAndCols(rli, 0, 19); // Back one character fi.emit('keypress', '.', { ctrl: true, name: 'b' }); assertCursorRowsAndCols(rli, 0, 18); // Back one character fi.emit('keypress', '.', { ctrl: true, name: 'b' }); assertCursorRowsAndCols(rli, 0, 17); // Forward one character fi.emit('keypress', '.', { ctrl: true, name: 'f' }); assertCursorRowsAndCols(rli, 0, 18); // Forward one character fi.emit('keypress', '.', { ctrl: true, name: 'f' }); assertCursorRowsAndCols(rli, 0, 19); rli.close(); } // Back and Forward one astral character { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', '💻'); // Move left one character/code point fi.emit('keypress', '.', { name: 'left' }); assertCursorRowsAndCols(rli, 0, 0); // Move right one character/code point fi.emit('keypress', '.', { name: 'right' }); assertCursorRowsAndCols(rli, 0, 2); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, '💻'); })); fi.emit('data', '\n'); rli.close(); } // Two astral characters left { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', '💻'); // Move left one character/code point fi.emit('keypress', '.', { name: 'left' }); assertCursorRowsAndCols(rli, 0, 0); fi.emit('data', '🐕'); assertCursorRowsAndCols(rli, 0, 2); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, '🐕💻'); })); fi.emit('data', '\n'); rli.close(); } // Two astral characters right { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', '💻'); // Move left one character/code point fi.emit('keypress', '.', { name: 'right' }); assertCursorRowsAndCols(rli, 0, 2); fi.emit('data', '🐕'); assertCursorRowsAndCols(rli, 0, 4); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, '💻🐕'); })); fi.emit('data', '\n'); rli.close(); } { // `wordLeft` and `wordRight` const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); fi.emit('keypress', '.', { ctrl: true, name: 'left' }); assertCursorRowsAndCols(rli, 0, 16); fi.emit('keypress', '.', { meta: true, name: 'b' }); assertCursorRowsAndCols(rli, 0, 10); fi.emit('keypress', '.', { ctrl: true, name: 'right' }); assertCursorRowsAndCols(rli, 0, 16); fi.emit('keypress', '.', { meta: true, name: 'f' }); assertCursorRowsAndCols(rli, 0, 19); rli.close(); } // `deleteWordLeft` [ { ctrl: true, name: 'w' }, { ctrl: true, name: 'backspace' }, { meta: true, name: 'backspace' }, ].forEach((deleteWordLeftKey) => { let [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); fi.emit('keypress', '.', { ctrl: true, name: 'left' }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'the quick fox'); })); fi.emit('keypress', '.', deleteWordLeftKey); fi.emit('data', '\n'); rli.close(); // No effect if pressed at beginning of line [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); fi.emit('keypress', '.', { ctrl: true, name: 'a' }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'the quick brown fox'); })); fi.emit('keypress', '.', deleteWordLeftKey); fi.emit('data', '\n'); rli.close(); }); // `deleteWordRight` [ { ctrl: true, name: 'delete' }, { meta: true, name: 'delete' }, { meta: true, name: 'd' }, ].forEach((deleteWordRightKey) => { let [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); fi.emit('keypress', '.', { ctrl: true, name: 'left' }); fi.emit('keypress', '.', { ctrl: true, name: 'left' }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'the quick fox'); })); fi.emit('keypress', '.', deleteWordRightKey); fi.emit('data', '\n'); rli.close(); // No effect if pressed at end of line [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'the quick brown fox'); })); fi.emit('keypress', '.', deleteWordRightKey); fi.emit('data', '\n'); rli.close(); }); // deleteLeft { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); assertCursorRowsAndCols(rli, 0, 19); // Delete left character fi.emit('keypress', '.', { ctrl: true, name: 'h' }); assertCursorRowsAndCols(rli, 0, 18); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'the quick brown fo'); })); fi.emit('data', '\n'); rli.close(); } // deleteLeft astral character { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', '💻'); assertCursorRowsAndCols(rli, 0, 2); // Delete left character fi.emit('keypress', '.', { ctrl: true, name: 'h' }); assertCursorRowsAndCols(rli, 0, 0); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, ''); })); fi.emit('data', '\n'); rli.close(); } // deleteRight { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); // Go to the start of the line fi.emit('keypress', '.', { ctrl: true, name: 'a' }); assertCursorRowsAndCols(rli, 0, 0); // Delete right character fi.emit('keypress', '.', { ctrl: true, name: 'd' }); assertCursorRowsAndCols(rli, 0, 0); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'he quick brown fox'); })); fi.emit('data', '\n'); rli.close(); } // deleteRight astral character { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', '💻'); // Go to the start of the line fi.emit('keypress', '.', { ctrl: true, name: 'a' }); assertCursorRowsAndCols(rli, 0, 0); // Delete right character fi.emit('keypress', '.', { ctrl: true, name: 'd' }); assertCursorRowsAndCols(rli, 0, 0); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, ''); })); fi.emit('data', '\n'); rli.close(); } // deleteLineLeft { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); assertCursorRowsAndCols(rli, 0, 19); // Delete from current to start of line fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'backspace' }); assertCursorRowsAndCols(rli, 0, 0); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, ''); })); fi.emit('data', '\n'); rli.close(); } // deleteLineRight { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); // Go to the start of the line fi.emit('keypress', '.', { ctrl: true, name: 'a' }); assertCursorRowsAndCols(rli, 0, 0); // Delete from current to end of line fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'delete' }); assertCursorRowsAndCols(rli, 0, 0); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, ''); })); fi.emit('data', '\n'); rli.close(); } // yank { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); assertCursorRowsAndCols(rli, 0, 19); // Go to the start of the line fi.emit('keypress', '.', { ctrl: true, name: 'a' }); // Move forward one char fi.emit('keypress', '.', { ctrl: true, name: 'f' }); // Delete the right part fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'delete' }); assertCursorRowsAndCols(rli, 0, 1); // Yank fi.emit('keypress', '.', { ctrl: true, name: 'y' }); assertCursorRowsAndCols(rli, 0, 19); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'the quick brown fox'); })); fi.emit('data', '\n'); rli.close(); } // yank pop { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); assertCursorRowsAndCols(rli, 0, 19); // Go to the start of the line fi.emit('keypress', '.', { ctrl: true, name: 'a' }); // Move forward one char fi.emit('keypress', '.', { ctrl: true, name: 'f' }); // Delete the right part fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'delete' }); assertCursorRowsAndCols(rli, 0, 1); // Yank fi.emit('keypress', '.', { ctrl: true, name: 'y' }); assertCursorRowsAndCols(rli, 0, 19); // Go to the start of the line fi.emit('keypress', '.', { ctrl: true, name: 'a' }); // Move forward four chars fi.emit('keypress', '.', { ctrl: true, name: 'f' }); fi.emit('keypress', '.', { ctrl: true, name: 'f' }); fi.emit('keypress', '.', { ctrl: true, name: 'f' }); fi.emit('keypress', '.', { ctrl: true, name: 'f' }); // Delete the right part fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'delete' }); assertCursorRowsAndCols(rli, 0, 4); // Go to the start of the line fi.emit('keypress', '.', { ctrl: true, name: 'a' }); assertCursorRowsAndCols(rli, 0, 0); // Yank: 'quick brown fox|the ' fi.emit('keypress', '.', { ctrl: true, name: 'y' }); // Yank pop: 'he quick brown fox|the' fi.emit('keypress', '.', { meta: true, name: 'y' }); assertCursorRowsAndCols(rli, 0, 18); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'he quick brown foxthe '); })); fi.emit('data', '\n'); rli.close(); } // Close readline interface { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('keypress', '.', { ctrl: true, name: 'c' }); assert(rli.closed); } // Multi-line input cursor position { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.columns = 10; fi.emit('data', 'multi-line text'); assertCursorRowsAndCols(rli, 1, 5); rli.close(); } // Multi-line input cursor position and long tabs { const [rli, fi] = getInterface({ tabSize: 16, terminal: true, prompt: '' }); fi.columns = 10; fi.emit('data', 'multi-line\ttext \t'); assert.strictEqual(rli.cursor, 17); assertCursorRowsAndCols(rli, 3, 2); rli.close(); } // Check for the default tab size. { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick\tbrown\tfox'); assert.strictEqual(rli.cursor, 19); // The first tab is 7 spaces long, the second one 3 spaces. assertCursorRowsAndCols(rli, 0, 27); } // Multi-line prompt cursor position { const [rli, fi] = getInterface({ terminal: true, prompt: '\nfilledline\nwraping text\n> ' }); fi.columns = 10; fi.emit('data', 't'); assertCursorRowsAndCols(rli, 4, 3); rli.close(); } // Undo & Redo { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); fi.emit('data', 'the quick brown fox'); assertCursorRowsAndCols(rli, 0, 19); // Delete the last eight chars fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', ',', { ctrl: true, shift: false, name: 'k' }); fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', '.', { ctrl: true, shift: false, name: 'b' }); fi.emit('keypress', ',', { ctrl: true, shift: false, name: 'k' }); assertCursorRowsAndCols(rli, 0, 11); // Perform undo twice fi.emit('keypress', ',', { sequence: '\x1F' }); assert.strictEqual(rli.line, 'the quick brown'); fi.emit('keypress', ',', { sequence: '\x1F' }); assert.strictEqual(rli.line, 'the quick brown fox'); // Perform redo twice fi.emit('keypress', ',', { sequence: '\x1E' }); assert.strictEqual(rli.line, 'the quick brown'); fi.emit('keypress', ',', { sequence: '\x1E' }); assert.strictEqual(rli.line, 'the quick b'); fi.emit('data', '\n'); rli.close(); } // Clear the whole screen { const [rli, fi] = getInterface({ terminal: true, prompt: '' }); const lines = ['line 1', 'line 2', 'line 3']; fi.emit('data', lines.join('\n')); fi.emit('keypress', '.', { ctrl: true, name: 'l' }); assertCursorRowsAndCols(rli, 0, 6); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'line 3'); })); fi.emit('data', '\n'); rli.close(); } // Wide characters should be treated as two columns. assert.strictEqual(getStringWidth('a'), 1); assert.strictEqual(getStringWidth('あ'), 2); assert.strictEqual(getStringWidth('谢'), 2); assert.strictEqual(getStringWidth('고'), 2); assert.strictEqual(getStringWidth(String.fromCodePoint(0x1f251)), 2); assert.strictEqual(getStringWidth('abcde'), 5); assert.strictEqual(getStringWidth('古池や'), 6); assert.strictEqual(getStringWidth('ノード.js'), 9); assert.strictEqual(getStringWidth('你好'), 4); assert.strictEqual(getStringWidth('안녕하세요'), 10); assert.strictEqual(getStringWidth('A\ud83c\ude00BC'), 5); assert.strictEqual(getStringWidth('👨‍👩‍👦‍👦'), 8); assert.strictEqual(getStringWidth('🐕𐐷あ💻😀'), 9); // TODO(BridgeAR): This should have a width of 4. assert.strictEqual(getStringWidth('⓬⓪'), 2); assert.strictEqual(getStringWidth('\u0301\u200D\u200E'), 0); // Check if vt control chars are stripped assert.strictEqual(stripVTControlCharacters('\u001b[31m> \u001b[39m'), '> '); assert.strictEqual( stripVTControlCharacters('\u001b[31m> \u001b[39m> '), '> > ' ); assert.strictEqual(stripVTControlCharacters('\u001b[31m\u001b[39m'), ''); assert.strictEqual(stripVTControlCharacters('> '), '> '); assert.strictEqual(getStringWidth('\u001b[31m> \u001b[39m'), 2); assert.strictEqual(getStringWidth('\u001b[31m> \u001b[39m> '), 4); assert.strictEqual(getStringWidth('\u001b[31m\u001b[39m'), 0); assert.strictEqual(getStringWidth('> '), 2); // Check EventEmitter memory leak for (let i = 0; i < 12; i++) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.close(); assert.strictEqual(isWarned(process.stdin._events), false); assert.strictEqual(isWarned(process.stdout._events), false); } [true, false].forEach((terminal) => { // Disable history { const [rli, fi] = getInterface({ terminal, historySize: 0 }); assert.strictEqual(rli.historySize, 0); fi.emit('data', 'asdf\n'); assert.deepStrictEqual(rli.history, []); rli.close(); } // Default history size 30 { const [rli, fi] = getInterface({ terminal }); assert.strictEqual(rli.historySize, 30); fi.emit('data', 'asdf\n'); assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : []); rli.close(); } // Sending a full line { const [rli, fi] = getInterface({ terminal }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'asdf'); })); fi.emit('data', 'asdf\n'); } // Ensure that options.signal.removeEventListener was called { const ac = new AbortController(); const signal = ac.signal; const [rli] = getInterface({ terminal }); signal.removeEventListener = common.mustCall( (event, onAbortFn) => { assert.strictEqual(event, 'abort'); assert.strictEqual(onAbortFn.name, 'onAbort'); }); rli.question('hello?', { signal }, common.mustCall()); rli.write('bar\n'); ac.abort(); rli.close(); } // Sending a blank line { const [rli, fi] = getInterface({ terminal }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, ''); })); fi.emit('data', '\n'); } // Sending a single character with no newline and then a newline { const [rli, fi] = getInterface({ terminal }); let called = false; rli.on('line', (line) => { called = true; assert.strictEqual(line, 'a'); }); fi.emit('data', 'a'); assert.ok(!called); fi.emit('data', '\n'); assert.ok(called); rli.close(); } // Sending multiple newlines at once { const [rli, fi] = getInterface({ terminal }); const expectedLines = ['foo', 'bar', 'baz']; rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLines.shift()); }, expectedLines.length)); fi.emit('data', `${expectedLines.join('\n')}\n`); rli.close(); } // Sending multiple newlines at once that does not end with a new line { const [rli, fi] = getInterface({ terminal }); const expectedLines = ['foo', 'bar', 'baz', 'bat']; rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLines.shift()); }, expectedLines.length - 1)); fi.emit('data', expectedLines.join('\n')); rli.close(); } // Sending multiple newlines at once that does not end with a new(empty) // line and a `end` event { const [rli, fi] = getInterface({ terminal }); const expectedLines = ['foo', 'bar', 'baz', '']; rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLines.shift()); }, expectedLines.length - 1)); rli.on('close', common.mustCall()); fi.emit('data', expectedLines.join('\n')); fi.emit('end'); rli.close(); } // Sending a multi-byte utf8 char over multiple writes { const buf = Buffer.from('☮', 'utf8'); const [rli, fi] = getInterface({ terminal }); let callCount = 0; rli.on('line', (line) => { callCount++; assert.strictEqual(line, buf.toString('utf8')); }); for (const i of buf) { fi.emit('data', Buffer.from([i])); } assert.strictEqual(callCount, 0); fi.emit('data', '\n'); assert.strictEqual(callCount, 1); rli.close(); } // Calling readline without `new` { const [rli, fi] = getInterface({ terminal }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'asdf'); })); fi.emit('data', 'asdf\n'); rli.close(); } // Calling the question callback { const [rli] = getInterface({ terminal }); rli.question('foo?', common.mustCall((answer) => { assert.strictEqual(answer, 'bar'); })); rli.write('bar\n'); rli.close(); } // Calling the question callback with abort signal { const [rli] = getInterface({ terminal }); const { signal } = new AbortController(); rli.question('foo?', { signal }, common.mustCall((answer) => { assert.strictEqual(answer, 'bar'); })); rli.write('bar\n'); rli.close(); } // Calling only the first question callback { const [rli] = getInterface({ terminal }); rli.question('foo?', common.mustCall((answer) => { assert.strictEqual(answer, 'bar'); })); rli.question('hello?', common.mustNotCall()); rli.write('bar\n'); } // Calling the question multiple times { const [rli] = getInterface({ terminal }); rli.question('foo?', common.mustCall((answer) => { assert.strictEqual(answer, 'baz'); })); rli.question('bar?', common.mustNotCall()); rli.write('baz\n'); rli.close(); } // Calling the promisified question { const [rli] = getInterface({ terminal }); const question = util.promisify(rli.question).bind(rli); question('foo?') .then(common.mustCall((answer) => { assert.strictEqual(answer, 'bar'); })); rli.write('bar\n'); rli.close(); } // Calling the promisified question with abort signal { const [rli] = getInterface({ terminal }); const question = util.promisify(rli.question).bind(rli); const { signal } = new AbortController(); question('foo?', { signal }) .then(common.mustCall((answer) => { assert.strictEqual(answer, 'bar'); })); rli.write('bar\n'); rli.close(); } // Aborting a question { const ac = new AbortController(); const signal = ac.signal; const [rli] = getInterface({ terminal }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'bar'); })); rli.question('hello?', { signal }, common.mustNotCall()); ac.abort(); rli.write('bar\n'); rli.close(); } // Aborting a promisified question { const ac = new AbortController(); const signal = ac.signal; const [rli] = getInterface({ terminal }); const question = util.promisify(rli.question).bind(rli); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'bar'); })); question('hello?', { signal }) .then(common.mustNotCall()) .catch(common.mustCall((error) => { assert.strictEqual(error.name, 'AbortError'); })); ac.abort(); rli.write('bar\n'); rli.close(); } // pre-aborted signal { const signal = AbortSignal.abort(); const [rli] = getInterface({ terminal }); rli.pause(); rli.on('resume', common.mustNotCall()); rli.question('hello?', { signal }, common.mustNotCall()); rli.close(); } // pre-aborted signal promisified question { const signal = AbortSignal.abort(); const [rli] = getInterface({ terminal }); const question = util.promisify(rli.question).bind(rli); rli.on('resume', common.mustNotCall()); rli.pause(); question('hello?', { signal }) .then(common.mustNotCall()) .catch(common.mustCall((error) => { assert.strictEqual(error.name, 'AbortError'); })); rli.close(); } // Call question after close { const [rli, fi] = getInterface({ terminal }); rli.question('What\'s your name?', common.mustCall((name) => { assert.strictEqual(name, 'Node.js'); rli.close(); assert.throws(() => { rli.question('How are you?', common.mustNotCall()); }, { name: 'Error', code: 'ERR_USE_AFTER_CLOSE' }); assert.notStrictEqual(rli.getPrompt(), 'How are you?'); })); fi.emit('data', 'Node.js\n'); } // Call promisified question after close { const [rli, fi] = getInterface({ terminal }); const question = util.promisify(rli.question).bind(rli); question('What\'s your name?').then(common.mustCall((name) => { assert.strictEqual(name, 'Node.js'); rli.close(); question('How are you?') .then(common.mustNotCall(), common.expectsError({ code: 'ERR_USE_AFTER_CLOSE', name: 'Error' })); assert.notStrictEqual(rli.getPrompt(), 'How are you?'); })); fi.emit('data', 'Node.js\n'); } // Can create a new readline Interface with a null output argument { const [rli, fi] = getInterface({ output: null, terminal }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'asdf'); })); fi.emit('data', 'asdf\n'); rli.setPrompt('ddd> '); rli.prompt(); rli.write("really shouldn't be seeing this"); rli.question('What do you think of node.js? ', (answer) => { console.log('Thank you for your valuable feedback:', answer); rli.close(); }); } // Calling the getPrompt method { const expectedPrompts = ['$ ', '> ']; const [rli] = getInterface({ terminal }); for (const prompt of expectedPrompts) { rli.setPrompt(prompt); assert.strictEqual(rli.getPrompt(), prompt); } } { const expected = terminal ? ['\u001b[1G', '\u001b[0J', '$ ', '\u001b[3G'] : ['$ ']; const output = new Writable({ write: common.mustCall((chunk, enc, cb) => { assert.strictEqual(chunk.toString(), expected.shift()); cb(); rl.close(); }, expected.length) }); const rl = readline.createInterface({ input: new Readable({ read: common.mustCall() }), output, prompt: '$ ', terminal }); rl.prompt(); assert.strictEqual(rl.getPrompt(), '$ '); } { const fi = new FakeInput(); assert.deepStrictEqual(fi.listeners(terminal ? 'keypress' : 'data'), []); } // Emit two line events when the delay // between \r and \n exceeds crlfDelay { const crlfDelay = 200; const [rli, fi] = getInterface({ terminal, crlfDelay }); let callCount = 0; rli.on('line', () => { callCount++; }); fi.emit('data', '\r'); setTimeout(common.mustCall(() => { fi.emit('data', '\n'); assert.strictEqual(callCount, 2); rli.close(); }), crlfDelay + 10); } // For the purposes of the following tests, we do not care about the exact // value of crlfDelay, only that the behaviour conforms to what's expected. // Setting it to Infinity allows the test to succeed even under extreme // CPU stress. const crlfDelay = Infinity; // Set crlfDelay to `Infinity` is allowed { const delay = 200; const [rli, fi] = getInterface({ terminal, crlfDelay }); let callCount = 0; rli.on('line', () => { callCount++; }); fi.emit('data', '\r'); setTimeout(common.mustCall(() => { fi.emit('data', '\n'); assert.strictEqual(callCount, 1); rli.close(); }), delay); } // Sending multiple newlines at once that does not end with a new line // and a `end` event(last line is) // \r\n should emit one line event, not two { const [rli, fi] = getInterface({ terminal, crlfDelay }); const expectedLines = ['foo', 'bar', 'baz', 'bat']; rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLines.shift()); }, expectedLines.length - 1)); fi.emit('data', expectedLines.join('\r\n')); rli.close(); } // \r\n should emit one line event when split across multiple writes. { const [rli, fi] = getInterface({ terminal, crlfDelay }); const expectedLines = ['foo', 'bar', 'baz', 'bat']; let callCount = 0; rli.on('line', common.mustCall((line) => { assert.strictEqual(line, expectedLines[callCount]); callCount++; }, expectedLines.length)); expectedLines.forEach((line) => { fi.emit('data', `${line}\r`); fi.emit('data', '\n'); }); rli.close(); } // Emit one line event when the delay between \r and \n is // over the default crlfDelay but within the setting value. { const delay = 125; const [rli, fi] = getInterface({ terminal, crlfDelay }); let callCount = 0; rli.on('line', () => callCount++); fi.emit('data', '\r'); setTimeout(common.mustCall(() => { fi.emit('data', '\n'); assert.strictEqual(callCount, 1); rli.close(); }), delay); } // Write correctly if paused { const [rli] = getInterface({ terminal }); rli.on('line', common.mustCall((line) => { assert.strictEqual(line, 'bar'); })); rli.pause(); rli.write('bar\n'); assert.strictEqual(rli.paused, false); rli.close(); } // Write undefined { const [rli] = getInterface({ terminal }); rli.on('line', common.mustNotCall()); rli.write(); rli.close(); } }); // Ensure that the _wordLeft method works even for large input { const input = new Readable({ read() { this.push('\x1B[1;5D'); // CTRL + Left this.push(null); }, }); const output = new Writable({ write: common.mustCall((data, encoding, cb) => { assert.strictEqual(rl.cursor, rl.line.length - 1); cb(); }), }); const rl = new readline.createInterface({ input, output, terminal: true, }); rl.line = `a${' '.repeat(1e6)}a`; rl.cursor = rl.line.length; } { const fi = new FakeInput(); const signal = AbortSignal.abort(); const rl = readline.createInterface({ input: fi, output: fi, signal, }); rl.on('close', common.mustCall()); assert.strictEqual(getEventListeners(signal, 'abort').length, 0); } { const fi = new FakeInput(); const ac = new AbortController(); const { signal } = ac; const rl = readline.createInterface({ input: fi, output: fi, signal, }); assert.strictEqual(getEventListeners(signal, 'abort').length, 1); rl.on('close', common.mustCall()); ac.abort(); assert.strictEqual(getEventListeners(signal, 'abort').length, 0); } { const fi = new FakeInput(); const ac = new AbortController(); const { signal } = ac; const rl = readline.createInterface({ input: fi, output: fi, signal, }); assert.strictEqual(getEventListeners(signal, 'abort').length, 1); rl.close(); assert.strictEqual(getEventListeners(signal, 'abort').length, 0); } { // Constructor throws if signal is not an abort signal assert.throws(() => { readline.createInterface({ input: new FakeInput(), signal: {}, }); }, { name: 'TypeError', code: 'ERR_INVALID_ARG_TYPE' }); }