summaryrefslogtreecommitdiff
path: root/tests/node_compat/test/parallel/test-readline-interface.js
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-02-10 13:22:13 -0700
committerGitHub <noreply@github.com>2024-02-10 20:22:13 +0000
commitf5e46c9bf2f50d66a953fa133161fc829cecff06 (patch)
tree8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/node_compat/test/parallel/test-readline-interface.js
parentd2477f780630a812bfd65e3987b70c0d309385bb (diff)
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests -> tests, and updates of relative paths for files. This is the first step towards aggregate all of the integration test files under tests/, which will lead to a set of integration tests that can run without the CLI binary being built. While we could leave these tests under `cli`, it would require us to keep a more complex directory structure for the various test runners. In addition, we have a lot of complexity to ignore various test files in the `cli` project itself (cargo publish exclusion rules, autotests = false, etc). And finally, the `tests/` folder will eventually house the `test_ffi`, `test_napi` and other testing code, reducing the size of the root repo directory. For easier review, the extremely large and noisy "move" is in the first commit (with no changes -- just a move), while the remainder of the changes to actual files is in the second commit.
Diffstat (limited to 'tests/node_compat/test/parallel/test-readline-interface.js')
-rw-r--r--tests/node_compat/test/parallel/test-readline-interface.js1217
1 files changed, 1217 insertions, 0 deletions
diff --git a/tests/node_compat/test/parallel/test-readline-interface.js b/tests/node_compat/test/parallel/test-readline-interface.js
new file mode 100644
index 000000000..e8e48dd1e
--- /dev/null
+++ b/tests/node_compat/test/parallel/test-readline-interface.js
@@ -0,0 +1,1217 @@
+// deno-fmt-ignore-file
+// deno-lint-ignore-file
+
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+// Taken from Node 16.13.0
+// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually
+
+// 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);
+}
+
+[
+ 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
+ ['not a number', -1, NaN, {}, true, Symbol(), null].forEach((historySize) => {
+ assert.throws(() => {
+ readline.createInterface({
+ input,
+ historySize,
+ });
+ }, {
+ name: 'RangeError',
+ code: 'ERR_INVALID_ARG_VALUE'
+ });
+ });
+
+ // Check for invalid tab sizes.
+ assert.throws(
+ () => new readline.Interface({
+ input,
+ tabSize: 0
+ }),
+ {
+ message: 'The value of "tabSize" is out of range. ' +
+ 'It must be >= 1 && < 4294967296. Received 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();
+}
+
+// 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();
+}
+
+// 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);
+
+// FIXME(bartlomieju): this causes hang
+// 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');
+ }
+
+ // 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 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();
+ }
+
+ // 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();
+ }
+
+ // 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);
+ }
+});
+
+// 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;
+}
+
+// FIXME(bartlomieju): these tests depend on "event_target" module
+// {
+// 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'
+ });
+}