From c97bbe6428f5d50f2a2dd96179209cb7d5dcee86 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Wed, 23 Apr 2025 22:17:35 -0700 Subject: [PATCH] readline: Fix readline-test-promises-csj.mjs (#19235) --- src/js/node/readline.ts | 14 +- .../parallel/test-readline-promises-csi.mjs | 227 ++++++++++++++++++ 2 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 test/js/node/test/parallel/test-readline-promises-csi.mjs diff --git a/src/js/node/readline.ts b/src/js/node/readline.ts index ffb92aa9e2..2779727702 100644 --- a/src/js/node/readline.ts +++ b/src/js/node/readline.ts @@ -2687,11 +2687,17 @@ class Readline { * flushed to the associated `stream`. */ commit() { - const { resolve, promise } = $newPromiseCapability(Promise); - this.#stream.write(ArrayPrototypeJoin.$call(this.#todo, ""), resolve); - this.#todo = []; + const { resolve, reject, promise } = $newPromiseCapability(Promise); - return promise; + try { + const data = ArrayPrototypeJoin.$call(this.#todo, ""); + this.#stream.write(data, resolve); + this.#todo = []; + } catch (err) { + reject(err); + } finally { + return promise; + } } /** diff --git a/test/js/node/test/parallel/test-readline-promises-csi.mjs b/test/js/node/test/parallel/test-readline-promises-csi.mjs new file mode 100644 index 0000000000..b6f9749f46 --- /dev/null +++ b/test/js/node/test/parallel/test-readline-promises-csi.mjs @@ -0,0 +1,227 @@ +// Flags: --expose-internals + + +import assert from 'assert'; +import { Readline } from 'readline/promises'; +import { Writable } from 'stream'; +import { setImmediate } from 'timers/promises'; +import '../common/index.mjs'; + +// import utils from 'internal/readline/utils'; +// const { CSI } = utils; +const { CSI } = require('readline')[Symbol.for("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")]; + +const INVALID_ARG = { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', +}; + +class TestWritable extends Writable { + data = ''; + _write(chunk, encoding, callback) { + this.data += chunk.toString(); + callback(); + } +} + +[ + undefined, null, + 0, 1, 1n, 1.1, NaN, Infinity, + true, false, + Symbol(), + '', '1', + [], {}, () => {}, +].forEach((arg) => + assert.throws(() => new Readline(arg), INVALID_ARG) +); + +{ + const writable = new TestWritable(); + const readline = new Readline(writable); + + await readline.clearScreenDown().commit(); + assert.deepStrictEqual(writable.data, CSI.kClearScreenDown); + await readline.clearScreenDown().commit(); + + writable.data = ''; + await readline.clearScreenDown().rollback(); + assert.strictEqual(writable.data, ''); + + writable.data = ''; + await readline.clearLine(-1).commit(); + assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning); + + writable.data = ''; + await readline.clearLine(1).commit(); + assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd); + + writable.data = ''; + await readline.clearLine(0).commit(); + assert.deepStrictEqual(writable.data, CSI.kClearLine); + + writable.data = ''; + await readline.clearLine(-1).commit(); + assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning); + + await readline.clearLine(0, null).commit(); + + // Nothing is written when moveCursor 0, 0 + for (const set of + [ + [0, 0, ''], + [1, 0, '\x1b[1C'], + [-1, 0, '\x1b[1D'], + [0, 1, '\x1b[1B'], + [0, -1, '\x1b[1A'], + [1, 1, '\x1b[1C\x1b[1B'], + [-1, 1, '\x1b[1D\x1b[1B'], + [-1, -1, '\x1b[1D\x1b[1A'], + [1, -1, '\x1b[1C\x1b[1A'], + ]) { + writable.data = ''; + await readline.moveCursor(set[0], set[1]).commit(); + assert.deepStrictEqual(writable.data, set[2]); + writable.data = ''; + await readline.moveCursor(set[0], set[1]).commit(); + assert.deepStrictEqual(writable.data, set[2]); + } + + + await readline.moveCursor(1, 1, null).commit(); + + writable.data = ''; + [ + undefined, null, + true, false, + Symbol(), + '', '1', + [], {}, () => {}, + ].forEach((arg) => + assert.throws(() => readline.cursorTo(arg), INVALID_ARG) + ); + assert.strictEqual(writable.data, ''); + + writable.data = ''; + assert.throws(() => readline.cursorTo('a', 'b'), INVALID_ARG); + assert.strictEqual(writable.data, ''); + + writable.data = ''; + assert.throws(() => readline.cursorTo('a', 1), INVALID_ARG); + assert.strictEqual(writable.data, ''); + + writable.data = ''; + assert.throws(() => readline.cursorTo(1, 'a'), INVALID_ARG); + assert.strictEqual(writable.data, ''); + + writable.data = ''; + await readline.cursorTo(1).commit(); + assert.strictEqual(writable.data, '\x1b[2G'); + + writable.data = ''; + await readline.cursorTo(1, 2).commit(); + assert.strictEqual(writable.data, '\x1b[3;2H'); + + writable.data = ''; + await readline.cursorTo(1, 2).commit(); + assert.strictEqual(writable.data, '\x1b[3;2H'); + + writable.data = ''; + await readline.cursorTo(1).cursorTo(1, 2).commit(); + assert.strictEqual(writable.data, '\x1b[2G\x1b[3;2H'); + + writable.data = ''; + await readline.cursorTo(1).commit(); + assert.strictEqual(writable.data, '\x1b[2G'); + + // Verify that cursorTo() rejects if x or y is NaN. + [1.1, NaN, Infinity].forEach((arg) => { + assert.throws(() => readline.cursorTo(arg), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); + }); + + [1.1, NaN, Infinity].forEach((arg) => { + assert.throws(() => readline.cursorTo(1, arg), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); + }); + + assert.throws(() => readline.cursorTo(NaN, NaN), { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); +} + +{ + const error = new Error(); + const writable = new class extends Writable { + _write() { throw error; } + }(); + const readline = new Readline(writable); + + await assert.rejects(readline.cursorTo(1).commit(), error); +} + +{ + const writable = new TestWritable(); + const readline = new Readline(writable, { autoCommit: true }); + + await readline.clearScreenDown(); + await setImmediate(); // Wait for next tick as auto commit is asynchronous. + assert.deepStrictEqual(writable.data, CSI.kClearScreenDown); +} + +{ + const writable = new TestWritable(); + const readline = new Readline(writable, { autoCommit: true }); + for (const [dir, data] of + [ + [-1, CSI.kClearToLineBeginning], + [1, CSI.kClearToLineEnd], + [0, CSI.kClearLine], + ]) { + writable.data = ''; + readline.clearLine(dir); + await setImmediate(); // Wait for next tick as auto commit is asynchronous. + assert.deepStrictEqual(writable.data, data); + } +} + +{ + const writable = new TestWritable(); + const readline = new Readline(writable, { autoCommit: true }); + for (const [x, y, data] of + [ + [0, 0, ''], + [1, 0, '\x1b[1C'], + [-1, 0, '\x1b[1D'], + [0, 1, '\x1b[1B'], + [0, -1, '\x1b[1A'], + [1, 1, '\x1b[1C\x1b[1B'], + [-1, 1, '\x1b[1D\x1b[1B'], + [-1, -1, '\x1b[1D\x1b[1A'], + [1, -1, '\x1b[1C\x1b[1A'], + ]) { + writable.data = ''; + readline.moveCursor(x, y); + await setImmediate(); // Wait for next tick as auto commit is asynchronous. + assert.deepStrictEqual(writable.data, data); + } +} + +{ + const writable = new TestWritable(); + const readline = new Readline(writable, { autoCommit: true }); + for (const [x, y, data] of + [ + [1, undefined, '\x1b[2G'], + [1, 2, '\x1b[3;2H'], + ]) { + writable.data = ''; + readline.cursorTo(x, y); + await setImmediate(); // Wait for next tick as auto commit is asynchronous. + assert.deepStrictEqual(writable.data, data); + } +} \ No newline at end of file