node/test/parallel/test-readline-interface.js
Ruben Bridgewater 8fb5fe28a4
util: improve unicode support
The array grouping function relies on the width of the characters.
It was not calculated correct so far, since it used the string
length instead.
This improves the unicode output by calculating the mono-spaced
font width (other fonts might differ).

PR-URL: https://github.com/nodejs/node/pull/31319
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Steven R Loomis <srloomis@us.ibm.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
2020-01-22 15:33:03 +01:00

1351 lines
39 KiB
JavaScript

// 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');
const assert = require('assert');
const readline = require('readline');
const {
getStringWidth,
stripVTControlCharacters
} = require('internal/util/inspect');
const EventEmitter = require('events').EventEmitter;
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;
}
{
// Default crlfDelay is 100ms
const fi = new FakeInput();
const rli = new readline.Interface({ input: fi, output: fi });
assert.strictEqual(rli.crlfDelay, 100);
rli.close();
}
{
// Minimum crlfDelay is 100ms
const fi = new FakeInput();
const rli = new readline.Interface({ input: fi, output: fi, crlfDelay: 0 });
assert.strictEqual(rli.crlfDelay, 100);
rli.close();
}
{
// Set crlfDelay to float 100.5ms
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
crlfDelay: 100.5
});
assert.strictEqual(rli.crlfDelay, 100.5);
rli.close();
}
{
// Set crlfDelay to 5000ms
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
crlfDelay: 5000
});
assert.strictEqual(rli.crlfDelay, 5000);
rli.close();
}
[ true, false ].forEach(function(terminal) {
// disable history
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal, historySize: 0 }
);
assert.strictEqual(rli.historySize, 0);
fi.emit('data', 'asdf\n');
assert.deepStrictEqual(rli.history, terminal ? [] : undefined);
rli.close();
}
// Default history size 30
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
assert.strictEqual(rli.historySize, 30);
fi.emit('data', 'asdf\n');
assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : undefined);
rli.close();
}
// sending a full line
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
let called = false;
rli.on('line', function(line) {
called = true;
assert.strictEqual(line, 'asdf');
});
fi.emit('data', 'asdf\n');
assert.ok(called);
}
// Sending a blank line
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
let called = false;
rli.on('line', function(line) {
called = true;
assert.strictEqual(line, '');
});
fi.emit('data', '\n');
assert.ok(called);
}
// Sending a single character with no newline
{
const fi = new FakeInput();
const rli = new readline.Interface(fi, {});
let called = false;
rli.on('line', function(line) {
called = true;
});
fi.emit('data', 'a');
assert.ok(!called);
rli.close();
}
// Sending a single character with no newline and then a newline
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
let called = false;
rli.on('line', function(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 fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
const expectedLines = ['foo', 'bar', 'baz'];
let callCount = 0;
rli.on('line', function(line) {
assert.strictEqual(line, expectedLines[callCount]);
callCount++;
});
fi.emit('data', `${expectedLines.join('\n')}\n`);
assert.strictEqual(callCount, expectedLines.length);
rli.close();
}
// Sending multiple newlines at once that does not end with a new line
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
const expectedLines = ['foo', 'bar', 'baz', 'bat'];
let callCount = 0;
rli.on('line', function(line) {
assert.strictEqual(line, expectedLines[callCount]);
callCount++;
});
fi.emit('data', expectedLines.join('\n'));
assert.strictEqual(callCount, expectedLines.length - 1);
rli.close();
}
// Sending multiple newlines at once that does not end with a new(empty)
// line and a `end` event
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
const expectedLines = ['foo', 'bar', 'baz', ''];
let callCount = 0;
rli.on('line', function(line) {
assert.strictEqual(line, expectedLines[callCount]);
callCount++;
});
rli.on('close', function() {
callCount++;
});
fi.emit('data', expectedLines.join('\n'));
fi.emit('end');
assert.strictEqual(callCount, expectedLines.length);
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 fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: true }
);
const expectedLines = ['foo', 'bar', 'baz', 'bat'];
let callCount = 0;
rli.on('line', function(line) {
assert.strictEqual(line, expectedLines[callCount]);
callCount++;
});
fi.emit('data', expectedLines.join('\r'));
assert.strictEqual(callCount, expectedLines.length - 1);
rli.close();
}
// \r at start of input should output blank line
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: true }
);
const expectedLines = ['', 'foo' ];
let callCount = 0;
rli.on('line', function(line) {
assert.strictEqual(line, expectedLines[callCount]);
callCount++;
});
fi.emit('data', '\rfoo\r');
assert.strictEqual(callCount, expectedLines.length);
rli.close();
}
// Emit two line events when the delay
// between \r and \n exceeds crlfDelay
{
const fi = new FakeInput();
const delay = 200;
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: terminal,
crlfDelay: delay
});
let callCount = 0;
rli.on('line', function(line) {
callCount++;
});
fi.emit('data', '\r');
setTimeout(common.mustCall(() => {
fi.emit('data', '\n');
assert.strictEqual(callCount, 2);
rli.close();
}), delay * 2);
}
// Set crlfDelay to `Infinity` is allowed
{
const fi = new FakeInput();
const delay = 200;
const crlfDelay = Infinity;
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: terminal,
crlfDelay
});
let callCount = 0;
rli.on('line', function(line) {
callCount++;
});
fi.emit('data', '\r');
setTimeout(common.mustCall(() => {
fi.emit('data', '\n');
assert.strictEqual(callCount, 1);
rli.close();
}), delay);
}
// \t when there is no completer function should behave like an ordinary
// character
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: true }
);
let called = false;
rli.on('line', function(line) {
assert.strictEqual(line, '\t');
assert.strictEqual(called, false);
called = true;
});
fi.emit('data', '\t');
fi.emit('data', '\n');
assert.ok(called);
rli.close();
}
// \t does not become part of the input when there is a completer function
{
const fi = new FakeInput();
const completer = (line) => [[], line];
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: true,
completer: completer
});
let called = false;
rli.on('line', function(line) {
assert.strictEqual(line, 'foo');
assert.strictEqual(called, false);
called = true;
});
for (const character of '\tfo\to\t') {
fi.emit('data', character);
}
fi.emit('data', '\n');
assert.ok(called);
rli.close();
}
// Constructor throws if completer is not a function or undefined
{
const fi = new FakeInput();
assert.throws(() => {
readline.createInterface({
input: fi,
completer: 'string is not valid'
});
}, {
name: 'TypeError',
code: 'ERR_INVALID_OPT_VALUE'
});
assert.throws(() => {
readline.createInterface({
input: fi,
completer: ''
});
}, {
name: 'TypeError',
code: 'ERR_INVALID_OPT_VALUE'
});
assert.throws(() => {
readline.createInterface({
input: fi,
completer: false
});
}, {
name: 'TypeError',
code: 'ERR_INVALID_OPT_VALUE'
});
}
// Constructor throws if historySize is not a positive number
{
const fi = new FakeInput();
assert.throws(() => {
readline.createInterface({
input: fi, historySize: 'not a number'
});
}, {
name: 'RangeError',
code: 'ERR_INVALID_OPT_VALUE'
});
assert.throws(() => {
readline.createInterface({
input: fi, historySize: -1
});
}, {
name: 'RangeError',
code: 'ERR_INVALID_OPT_VALUE'
});
assert.throws(() => {
readline.createInterface({
input: fi, historySize: NaN
});
}, {
name: 'RangeError',
code: 'ERR_INVALID_OPT_VALUE'
});
}
// Duplicate lines are removed from history when
// `options.removeHistoryDuplicates` is `true`
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: true,
removeHistoryDuplicates: true
});
const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
// ['foo', 'baz', 'bar', bat'];
let callCount = 0;
rli.on('line', function(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 fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: true,
removeHistoryDuplicates: false
});
const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat'];
let callCount = 0;
rli.on('line', function(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();
}
// Sending a multi-byte utf8 char over multiple writes
{
const buf = Buffer.from('☮', 'utf8');
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
let callCount = 0;
rli.on('line', function(line) {
callCount++;
assert.strictEqual(line, buf.toString('utf8'));
});
[].forEach.call(buf, function(i) {
fi.emit('data', Buffer.from([i]));
});
assert.strictEqual(callCount, 0);
fi.emit('data', '\n');
assert.strictEqual(callCount, 1);
rli.close();
}
// Regression test for repl freeze, #1968:
// check that nothing fails if 'keypress' event throws.
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: true }
);
const keys = [];
const err = new Error('bad thing happened');
fi.on('keypress', function(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();
}
// Calling readline without `new`
{
const fi = new FakeInput();
const rli = readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
let called = false;
rli.on('line', function(line) {
called = true;
assert.strictEqual(line, 'asdf');
});
fi.emit('data', 'asdf\n');
assert.ok(called);
rli.close();
}
// Calling the question callback
{
let called = false;
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
rli.question('foo?', function(answer) {
called = true;
assert.strictEqual(answer, 'bar');
});
rli.write('bar\n');
assert.ok(called);
rli.close();
}
if (terminal) {
// history is bound
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal, 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 fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
const expectedLines = ['foo'];
rli.question(expectedLines[0], function() {
rli.close();
});
const cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, expectedLines[0].length);
rli.close();
}
// Sending a multi-line question
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: fi, terminal: terminal }
);
const expectedLines = ['foo', 'bar'];
rli.question(expectedLines.join('\n'), function() {
rli.close();
});
const cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, expectedLines.length - 1);
assert.strictEqual(cursorPos.cols, expectedLines.slice(-1)[0].length);
rli.close();
}
{
// Beginning and end of line
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', 'the quick brown fox');
fi.emit('keypress', '.', { ctrl: true, name: 'a' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
fi.emit('keypress', '.', { ctrl: true, name: 'e' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 19);
rli.close();
}
{
// Back and Forward one character
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', 'the quick brown fox');
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 19);
// Back one character
fi.emit('keypress', '.', { ctrl: true, name: 'b' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 18);
// Back one character
fi.emit('keypress', '.', { ctrl: true, name: 'b' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 17);
// Forward one character
fi.emit('keypress', '.', { ctrl: true, name: 'f' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 18);
// Forward one character
fi.emit('keypress', '.', { ctrl: true, name: 'f' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 19);
rli.close();
}
// Back and Forward one astral character
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');
// Move left one character/code point
fi.emit('keypress', '.', { name: 'left' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
// Move right one character/code point
fi.emit('keypress', '.', { name: 'right' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 2);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '💻');
}));
fi.emit('data', '\n');
rli.close();
}
// Two astral characters left
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');
// Move left one character/code point
fi.emit('keypress', '.', { name: 'left' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
fi.emit('data', '🐕');
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 2);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '🐕💻');
}));
fi.emit('data', '\n');
rli.close();
}
// Two astral characters right
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');
// Move left one character/code point
fi.emit('keypress', '.', { name: 'right' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 2);
fi.emit('data', '🐕');
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 4);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '💻🐕');
}));
fi.emit('data', '\n');
rli.close();
}
{
// `wordLeft` and `wordRight`
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', 'the quick brown fox');
fi.emit('keypress', '.', { ctrl: true, name: 'left' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 16);
fi.emit('keypress', '.', { meta: true, name: 'b' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 10);
fi.emit('keypress', '.', { ctrl: true, name: 'right' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 16);
fi.emit('keypress', '.', { meta: true, name: 'f' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 19);
rli.close();
}
{
// `deleteWordLeft`
[
{ ctrl: true, name: 'w' },
{ ctrl: true, name: 'backspace' },
{ meta: true, name: 'backspace' }
]
.forEach((deleteWordLeftKey) => {
let fi = new FakeInput();
let rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
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
fi = new FakeInput();
rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
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 fi = new FakeInput();
let rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
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
fi = new FakeInput();
rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
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 fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', 'the quick brown fox');
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 19);
// Delete left character
fi.emit('keypress', '.', { ctrl: true, name: 'h' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 18);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, 'the quick brown fo');
}));
fi.emit('data', '\n');
rli.close();
}
// deleteLeft astral character
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 2);
// Delete left character
fi.emit('keypress', '.', { ctrl: true, name: 'h' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '');
}));
fi.emit('data', '\n');
rli.close();
}
// deleteRight
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', 'the quick brown fox');
// Go to the start of the line
fi.emit('keypress', '.', { ctrl: true, name: 'a' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
// Delete right character
fi.emit('keypress', '.', { ctrl: true, name: 'd' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, 'he quick brown fox');
}));
fi.emit('data', '\n');
rli.close();
}
// deleteRight astral character
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', '💻');
// Go to the start of the line
fi.emit('keypress', '.', { ctrl: true, name: 'a' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
// Delete right character
fi.emit('keypress', '.', { ctrl: true, name: 'd' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '');
}));
fi.emit('data', '\n');
rli.close();
}
// deleteLineLeft
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', 'the quick brown fox');
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 19);
// Delete from current to start of line
fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'backspace' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '');
}));
fi.emit('data', '\n');
rli.close();
}
// deleteLineRight
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.emit('data', 'the quick brown fox');
// Go to the start of the line
fi.emit('keypress', '.', { ctrl: true, name: 'a' });
let cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
// Delete from current to end of line
fi.emit('keypress', '.', { ctrl: true, shift: true, name: 'delete' });
cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 0);
rli.on('line', common.mustCall((line) => {
assert.strictEqual(line, '');
}));
fi.emit('data', '\n');
rli.close();
}
// Multi-line input cursor position
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
fi.columns = 10;
fi.emit('data', 'multi-line text');
const cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 1);
assert.strictEqual(cursorPos.cols, 5);
rli.close();
}
// Multi-line prompt cursor position
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '\nfilledline\nwraping text\n> ',
terminal: terminal
});
fi.columns = 10;
fi.emit('data', 't');
const cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 4);
assert.strictEqual(cursorPos.cols, 3);
rli.close();
}
// Clear the whole screen
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
prompt: '',
terminal: terminal
});
const lines = ['line 1', 'line 2', 'line 3'];
fi.emit('data', lines.join('\n'));
fi.emit('keypress', '.', { ctrl: true, name: 'l' });
const cursorPos = rli.getCursorPos();
assert.strictEqual(cursorPos.rows, 0);
assert.strictEqual(cursorPos.cols, 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);
{
const fi = new FakeInput();
assert.deepStrictEqual(fi.listeners(terminal ? 'keypress' : 'data'), []);
}
// 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);
}
// Can create a new readline Interface with a null output argument
{
const fi = new FakeInput();
const rli = new readline.Interface(
{ input: fi, output: null, terminal: terminal }
);
let called = false;
rli.on('line', function(line) {
called = true;
assert.strictEqual(line, 'asdf');
});
fi.emit('data', 'asdf\n');
assert.ok(called);
rli.setPrompt('ddd> ');
rli.prompt();
rli.write('really shouldnt be seeing this');
rli.question('What do you think of node.js? ', function(answer) {
console.log('Thank you for your valuable feedback:', answer);
rli.close();
});
}
{
const expected = terminal ?
['\u001b[1G', '\u001b[0J', '$ ', '\u001b[3G'] :
['$ '];
let counter = 0;
const output = new Writable({
write: common.mustCall((chunk, enc, cb) => {
assert.strictEqual(chunk.toString(), expected[counter++]);
cb();
rl.close();
}, expected.length)
});
const rl = readline.createInterface({
input: new Readable({ read: common.mustCall() }),
output: output,
prompt: '$ ',
terminal: terminal
});
rl.prompt();
assert.strictEqual(rl._prompt, '$ ');
}
});
// 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;
[ true, false ].forEach(function(terminal) {
// 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 fi = new FakeInput();
const rli = new readline.Interface(
{
input: fi,
output: fi,
terminal: terminal,
crlfDelay
}
);
const expectedLines = ['foo', 'bar', 'baz', 'bat'];
let callCount = 0;
rli.on('line', function(line) {
assert.strictEqual(line, expectedLines[callCount]);
callCount++;
});
fi.emit('data', expectedLines.join('\r\n'));
assert.strictEqual(callCount, expectedLines.length - 1);
rli.close();
}
// \r\n should emit one line event when split across multiple writes.
{
const fi = new FakeInput();
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: terminal,
crlfDelay
});
const expectedLines = ['foo', 'bar', 'baz', 'bat'];
let callCount = 0;
rli.on('line', function(line) {
assert.strictEqual(line, expectedLines[callCount]);
callCount++;
});
expectedLines.forEach(function(line) {
fi.emit('data', `${line}\r`);
fi.emit('data', '\n');
});
assert.strictEqual(callCount, expectedLines.length);
rli.close();
}
// Emit one line event when the delay between \r and \n is
// over the default crlfDelay but within the setting value.
{
const fi = new FakeInput();
const delay = 125;
const rli = new readline.Interface({
input: fi,
output: fi,
terminal: 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: input,
output: output,
terminal: true,
});
rl.line = `a${' '.repeat(1e6)}a`;
rl.cursor = rl.line.length;
}