From 086199338d25b6e0d924e2b7e3418438582801f4 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 30 Mar 2021 10:11:29 +0200 Subject: [PATCH] fixup! readline: add Promise-based API --- lib/readline.js | 22 ++-- lib/readline/promises.js | 4 + test/common/index.js | 17 ++- .../test-readline-promises-tab-complete.js | 116 ++++++++++++++++++ test/parallel/test-readline-tab-complete.js | 48 ++++++-- 5 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 test/parallel/test-readline-promises-tab-complete.js diff --git a/lib/readline.js b/lib/readline.js index 443eaec321fbd1..155cf01c9ef0e7 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -124,16 +124,11 @@ Interface.prototype.question = function(query, options, cb) { if (options.signal) { if (options.signal.aborted) { - this[kQuestionCancel](); return; } - options.signal.addEventListener( - 'abort', - () => { - this[kQuestionCancel](); - }, - { once: true } - ); + options.signal.addEventListener('abort', () => { + this[kQuestionCancel](); + }, { once: true }); } if (typeof cb === 'function') { FunctionPrototypeCall(_Interface.prototype.question, this, @@ -377,7 +372,8 @@ function _ttyWriteDumb(s, key) { if (key.name === 'escape') return; - if (this[kSawReturnAt] && key.name !== 'enter') this[kSawReturnAt] = 0; + if (this[kSawReturnAt] && key.name !== 'enter') + this[kSawReturnAt] = 0; if (key.ctrl) { if (key.name === 'c') { @@ -396,17 +392,15 @@ function _ttyWriteDumb(s, key) { } switch (key.name) { - case 'return': // Carriage return, i.e. \r + case 'return': // Carriage return, i.e. \r this[kSawReturnAt] = DateNow(); this[kLine](); break; case 'enter': // When key interval > crlfDelay - if ( - this[kSawReturnAt] === 0 || - DateNow() - this[kSawReturnAt] > this.crlfDelay - ) { + if (this[kSawReturnAt] === 0 || + DateNow() - this[kSawReturnAt] > this.crlfDelay) { this[kLine](); } this[kSawReturnAt] = 0; diff --git a/lib/readline/promises.js b/lib/readline/promises.js index 5ccf9da58522df..17a4eaad74c34c 100644 --- a/lib/readline/promises.js +++ b/lib/readline/promises.js @@ -21,6 +21,10 @@ const { } = require('internal/errors'); class Interface extends _Interface { + // eslint-disable-next-line no-useless-constructor + constructor(input, output, completer, terminal) { + super(input, output, completer, terminal); + } question(query, options = {}) { return new Promise((resolve, reject) => { if (options.signal) { diff --git a/test/common/index.js b/test/common/index.js index 03eec756d0a19a..57108cac3e2006 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -383,10 +383,25 @@ function _mustCallInner(fn, criteria = 1, field) { mustCallChecks.push(context); - return function() { + const _return = function() { // eslint-disable-line func-style context.actual++; return fn.apply(this, arguments); }; + Object.defineProperties(_return, { + name: { + value: fn.name, + writable: false, + enumerable: false, + configurable: true, + }, + length: { + value: fn.length, + writable: false, + enumerable: false, + configurable: true, + }, + }); + return _return; } function hasMultiLocalhost() { diff --git a/test/parallel/test-readline-promises-tab-complete.js b/test/parallel/test-readline-promises-tab-complete.js new file mode 100644 index 00000000000000..e57cfe94337383 --- /dev/null +++ b/test/parallel/test-readline-promises-tab-complete.js @@ -0,0 +1,116 @@ +'use strict'; + +// Flags: --expose-internals + +const common = require('../common'); +const readline = require('readline/promises'); +const assert = require('assert'); +const { EventEmitter } = require('events'); +const { getStringWidth } = require('internal/util/inspect'); + +common.skipIfDumbTerminal(); + +// This test verifies that the tab completion supports unicode and the writes +// are limited to the minimum. +[ + 'あ', + '𐐷', + '🐕' +].forEach((char) => { + [true, false].forEach((lineBreak) => { + [ + (line) => [ + ['First group', '', + `${char}${'a'.repeat(10)}`, + `${char}${'b'.repeat(10)}`, + char.repeat(11), + ], + line + ], + + async (line) => [ + ['First group', '', + `${char}${'a'.repeat(10)}`, + `${char}${'b'.repeat(10)}`, + char.repeat(11), + ], + line + ], + ].forEach((completer) => { + + let output = ''; + const width = getStringWidth(char) - 1; + + class FakeInput extends EventEmitter { + columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3 + + write = common.mustCall((data) => { + output += data; + }, 6) + + resume() {} + pause() {} + end() {} + } + + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + completer: common.mustCallAtLeast(completer), + }); + + const last = '\r\nFirst group\r\n\r\n' + + `${char}${'a'.repeat(10)}${' '.repeat(2 + width * 10)}` + + `${char}${'b'.repeat(10)}` + + (lineBreak ? '\r\n' : ' '.repeat(2 + width * 10)) + + `${char.repeat(11)}\r\n` + + `\r\n\u001b[1G\u001b[0J> ${char}\u001b[${4 + width}G`; + + const expectations = [char, '', last]; + + rli.on('line', common.mustNotCall()); + for (const character of `${char}\t\t`) { + fi.emit('data', character); + queueMicrotask(() => { + assert.strictEqual(output, expectations.shift()); + output = ''; + }); + } + rli.close(); + }); + }); +}); + +{ + let output = ''; + class FakeInput extends EventEmitter { + columns = 80 + + write = common.mustCall((data) => { + output += data; + }, 1) + + resume() {} + pause() {} + end() {} + } + + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + completer: + common.mustCallAtLeast(() => Promise.reject(new Error('message'))), + }); + + rli.on('line', common.mustNotCall()); + fi.emit('data', '\t'); + queueMicrotask(() => { + assert.match(output, /^Tab completion error: Error: message/); + output = ''; + }); + rli.close(); +} diff --git a/test/parallel/test-readline-tab-complete.js b/test/parallel/test-readline-tab-complete.js index e8871863402cb1..e8dcbeebe107ad 100644 --- a/test/parallel/test-readline-tab-complete.js +++ b/test/parallel/test-readline-tab-complete.js @@ -31,15 +31,15 @@ common.skipIfDumbTerminal(); const width = getStringWidth(char) - 1; class FakeInput extends EventEmitter { - columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3 + columns = ((width + 1) * 10 + (lineBreak ? 0 : 10)) * 3 - write = common.mustCall((data) => { - output += data; - }, 6) + write = common.mustCall((data) => { + output += data; + }, 6) - resume() {} - pause() {} - end() {} + resume() {} + pause() {} + end() {} } const fi = new FakeInput(); @@ -47,7 +47,7 @@ common.skipIfDumbTerminal(); input: fi, output: fi, terminal: true, - completer: completer + completer: common.mustCallAtLeast(completer), }); const last = '\r\nFirst group\r\n\r\n' + @@ -68,3 +68,35 @@ common.skipIfDumbTerminal(); rli.close(); }); }); + +{ + let output = ''; + class FakeInput extends EventEmitter { + columns = 80 + + write = common.mustCall((data) => { + output += data; + }, 1) + + resume() {} + pause() {} + end() {} + } + + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + completer: + common.mustCallAtLeast((_, cb) => cb(new Error('message'))), + }); + + rli.on('line', common.mustNotCall()); + fi.emit('data', '\t'); + queueMicrotask(() => { + assert.match(output, /^Tab completion error: Error: message/); + output = ''; + }); + rli.close(); +}