diff options
author | 2023-01-08 03:49:49 -0600 | |
---|---|---|
committer | 2023-01-08 01:49:49 -0800 | |
commit | 94409770dece8bb9dfc23f4bdc2f240836035d87 (patch) | |
tree | 4cc627eb67c476871e84141a6c7a583e29e98309 /test/bun.js | |
parent | c505f172b84f5359aa186513f3ef7d6394bfc7b2 (diff) | |
download | bun-94409770dece8bb9dfc23f4bdc2f240836035d87.tar.gz bun-94409770dece8bb9dfc23f4bdc2f240836035d87.tar.zst bun-94409770dece8bb9dfc23f4bdc2f240836035d87.zip |
feat(node:readline): add node:readline and node:readline/promises (#1738)
* feat(readline): WIP: add readline
* test(helpers): add deepStrictEqual helper
* feat(readline): add readline & readline/promises to loader
* fix(node:events): emit newListener on new listener added
* feat(readline): finish readline cb interface, add tests
* fix(stream): fix Transform.end()
* fix(node-test-helpers): correct throws behavior, improve how all asserts work
* feat(readline/promises): add readline/promises
* feat(assert): add assert.match
* test(readline): uncomment more tests
* fix(readline): MaxCeil -> MathCeil đ¤Ś
* fix(readline): export promises from node:readline
* fix(readline): temp fix for circular dependency
* cleanup(readline): remove console.log
* fix(readline): change true -> 0 for CommonJS export
* perf(readline): micro-optimizations with some getters
* perf(readline): lazy load isWritable
* cleanup(readline): rename debug flag env var to BUN_JS_DEBUG
Diffstat (limited to 'test/bun.js')
-rw-r--r-- | test/bun.js/node-test-helpers.js | 156 | ||||
-rw-r--r-- | test/bun.js/node-test-helpers.ts | 198 | ||||
-rw-r--r-- | test/bun.js/readline.node.test.ts | 2018 | ||||
-rw-r--r-- | test/bun.js/readline_promises.node.test.ts | 55 |
4 files changed, 2271 insertions, 156 deletions
diff --git a/test/bun.js/node-test-helpers.js b/test/bun.js/node-test-helpers.js deleted file mode 100644 index f62f1ab3b..000000000 --- a/test/bun.js/node-test-helpers.js +++ /dev/null @@ -1,156 +0,0 @@ -import { expect as expect_ } from "bun:test"; -import { gcTick } from "gc"; -import assertNode from "node:assert"; - -const expect = (actual) => { - gcTick(); - const ret = expect_(actual); - gcTick(); - return ret; -}; - -export const strictEqual = (...args) => { - let error = null; - try { - assertNode.strictEqual(...args); - } catch (err) { - error = err; - } - expect(error).toBe(null); -}; - -export const throws = (...args) => { - let error = null; - try { - assertNode.throws(...args); - } catch (err) { - error = err; - } - expect(error).toBe(null); -}; - -export const assert = (...args) => { - let error = null; - try { - assertNode(...args); - } catch (err) { - error = err; - } - expect(error).toBe(null); -}; - -export const assertOk = (...args) => { - let error = null; - try { - assertNode.ok(...args); - } catch (err) { - error = err; - } - expect(error).toBe(null); -}; - -export const createCallCheckCtx = (done, timeout = 1500) => { - const createDone = createDoneDotAll(done); - // const mustCallChecks = []; - - // failed.forEach(function (context) { - // console.log( - // "Mismatched %s function calls. Expected %s, actual %d.", - // context.name, - // context.messageSegment, - // context.actual - // ); - // console.log(context.stack.split("\n").slice(2).join("\n")); - // }); - - // TODO: Implement this to be exact only - function mustCall(fn, exact) { - return mustCallAtLeast(fn, exact); - } - - function mustSucceed(fn, exact) { - return mustCall(function (err, ...args) { - assert.ifError(err); - if (typeof fn === "function") return fn.apply(this, args); - }, exact); - } - - function mustCallAtLeast(fn, minimum) { - return _mustCallInner(fn, minimum, "minimum"); - } - - function _mustCallInner(fn, criteria = 1, field) { - if (process._exiting) - throw new Error("Cannot use common.mustCall*() in process exit handler"); - if (typeof fn === "number") { - criteria = fn; - fn = noop; - } else if (fn === undefined) { - fn = noop; - } - - if (typeof criteria !== "number") - throw new TypeError(`Invalid ${field} value: ${criteria}`); - - let actual = 0; - let expected = criteria; - - // mustCallChecks.push(context); - const done = createDone(timeout); - const _return = (...args) => { - const result = fn.apply(this, args); - actual++; - if (actual >= expected) { - done(); - } - return result; - }; - // Function instances have own properties that may be relevant. - // Let's replicate those properties to the returned function. - // Refs: https://tc39.es/ecma262/#sec-function-instances - 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; - } - return { - mustSucceed, - mustCall, - mustCallAtLeast, - }; -}; - -export function createDoneDotAll(done) { - let toComplete = 0; - let completed = 0; - function createDoneCb(timeout) { - toComplete += 1; - const timer = setTimeout(() => { - console.log("Timeout"); - done(new Error("Timed out!")); - }, timeout); - return (result) => { - clearTimeout(timer); - if (result instanceof Error) { - done(result); - return; - } - completed += 1; - if (completed === toComplete) { - done(); - } - }; - } - return createDoneCb; -} diff --git a/test/bun.js/node-test-helpers.ts b/test/bun.js/node-test-helpers.ts new file mode 100644 index 000000000..ff7b8ece3 --- /dev/null +++ b/test/bun.js/node-test-helpers.ts @@ -0,0 +1,198 @@ +import { expect as expect_ } from "bun:test"; +// @ts-ignore +import { gcTick } from "gc"; +import assertNode from "node:assert"; + +type DoneCb = (err?: Error) => any; +function noop() {} + +const expect = (actual) => { + gcTick(); + const ret = expect_(actual); + gcTick(); + return ret; +}; + +// Assert +export const strictEqual = ( + ...args: Parameters<typeof assertNode.strictEqual> +) => { + assertNode.strictEqual.apply(this, args); + expect(true).toBe(true); +}; + +export const notStrictEqual = ( + ...args: Parameters<typeof assertNode.notStrictEqual> +) => { + assertNode.notStrictEqual.apply(this, args); + expect(true).toBe(true); +}; + +export const deepStrictEqual = ( + ...args: Parameters<typeof assertNode.deepStrictEqual> +) => { + assertNode.deepStrictEqual.apply(this, args); + expect(true).toBe(true); +}; + +export const throws = (...args: Parameters<typeof assertNode.throws>) => { + assertNode.throws.apply(this, args); + expect(true).toBe(true); +}; + +export const ok = (...args: Parameters<typeof assertNode.ok>) => { + assertNode.ok.apply(this, args); + expect(true).toBe(true); +}; + +export const ifError = (...args: Parameters<typeof assertNode.ifError>) => { + assertNode.ifError.apply(this, args); + expect(true).toBe(true); +}; + +export const match = (...args: Parameters<typeof assertNode.match>) => { + assertNode.match.apply(this, args); + expect(true).toBe(true); +}; + +export const assert = { + strictEqual, + deepStrictEqual, + notStrictEqual, + throws, + ok, + ifError, + match, +}; + +// End assert + +export const createCallCheckCtx = (done: DoneCb) => { + const createDone = createDoneDotAll(done); + + // const mustCallChecks = []; + + // failed.forEach(function (context) { + // console.log( + // "Mismatched %s function calls. Expected %s, actual %d.", + // context.name, + // context.messageSegment, + // context.actual + // ); + // console.log(context.stack.split("\n").slice(2).join("\n")); + // }); + + // TODO: Implement this to be exact only + function mustCall(fn?: (...args) => any, exact?: number) { + return mustCallAtLeast(fn, exact); + } + + function mustNotCall( + reason: string = "function should not have been called", + ) { + const localDone = createDone(); + setTimeout(() => localDone(), 200); + return () => { + done(new Error(reason)); + }; + } + + function mustSucceed(fn: () => any, exact?: number) { + return mustCall(function (err, ...args) { + ifError(err); + // @ts-ignore + if (typeof fn === "function") return fn.apply(this, args as []); + }, exact); + } + + function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, "minimum"); + } + + function _mustCallInner(fn, criteria = 1, field) { + if (process._exiting) + throw new Error("Cannot use common.mustCall*() in process exit handler"); + if (typeof fn === "number") { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== "number") + throw new TypeError(`Invalid ${field} value: ${criteria}`); + + let actual = 0; + let expected = criteria; + + // mustCallChecks.push(context); + const done = createDone(); + const _return = (...args) => { + // @ts-ignore + const result = fn.apply(this, args); + actual++; + if (actual >= expected) { + done(); + } + return result; + }; + // Function instances have own properties that may be relevant. + // Let's replicate those properties to the returned function. + // Refs: https://tc39.es/ecma262/#sec-function-instances + 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; + } + return { + mustSucceed, + mustCall, + mustCallAtLeast, + mustNotCall, + }; +}; + +export function createDoneDotAll(done: DoneCb, globalTimeout?: number) { + let toComplete = 0; + let completed = 0; + const globalTimer = globalTimeout + ? setTimeout(() => { + console.log("Global Timeout"); + done(new Error("Timed out!")); + }, globalTimeout) + : undefined; + function createDoneCb(timeout?: number) { + toComplete += 1; + const timer = + timeout !== undefined + ? setTimeout(() => { + console.log("Timeout"); + done(new Error("Timed out!")); + }, timeout) + : timeout; + return (result?: Error) => { + if (timer) clearTimeout(timer); + if (globalTimer) clearTimeout(globalTimer); + if (result instanceof Error) { + done(result); + return; + } + completed += 1; + if (completed === toComplete) { + done(); + } + }; + } + return createDoneCb; +} diff --git a/test/bun.js/readline.node.test.ts b/test/bun.js/readline.node.test.ts new file mode 100644 index 000000000..c681a8e15 --- /dev/null +++ b/test/bun.js/readline.node.test.ts @@ -0,0 +1,2018 @@ +import { beforeEach, describe, it } from "bun:test"; +import readline from "node:readline"; +import { Writable, PassThrough } from "node:stream"; +import { EventEmitter } from "node:events"; +import { + createDoneDotAll, + createCallCheckCtx, + assert, +} from "./node-test-helpers"; + +var { + CSI, + utils: { getStringWidth, stripVTControlCharacters }, +} = readline[Symbol.for("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")]; + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +class TestWritable extends Writable { + data; + constructor() { + super(); + this.data = ""; + } + _write(chunk, encoding, callback) { + this.data += chunk.toString(); + callback(); + } +} + +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 writable = new TestWritable(); +const input = new FakeInput(); + +// ---------------------------------------------------------------------------- +// Tests +// ---------------------------------------------------------------------------- + +describe("CSI", () => { + it("should be defined", () => { + assert.ok(CSI); + }); + + it("should have all the correct clear sequences", () => { + assert.strictEqual(CSI.kClearToLineBeginning, "\x1b[1K"); + assert.strictEqual(CSI.kClearToLineEnd, "\x1b[0K"); + assert.strictEqual(CSI.kClearLine, "\x1b[2K"); + assert.strictEqual(CSI.kClearScreenDown, "\x1b[0J"); + assert.strictEqual(CSI`1${2}3`, "\x1b[123"); + }); +}); + +describe("readline.clearScreenDown()", () => { + it("should put clear screen sequence into writable when called", (done) => { + const { mustCall } = createCallCheckCtx(done); + + assert.strictEqual(readline.clearScreenDown(writable), true); + assert.deepStrictEqual(writable.data, CSI.kClearScreenDown); + assert.strictEqual(readline.clearScreenDown(writable, mustCall()), true); + }); + + it("should throw on invalid callback", () => { + // Verify that clearScreenDown() throws on invalid callback. + assert.throws(() => { + readline.clearScreenDown(writable, null); + }, /ERR_INVALID_ARG_TYPE/); + }); + + it("should that clearScreenDown() does not throw on null or undefined stream", (done) => { + const { mustCall } = createCallCheckCtx(done); + assert.strictEqual( + readline.clearScreenDown( + null, + mustCall((err) => { + assert.strictEqual(err, null); + }), + ), + true, + ); + assert.strictEqual(readline.clearScreenDown(undefined, mustCall()), true); + }); +}); + +describe("readline.clearLine()", () => { + beforeEach(() => { + writable.data = ""; + }); + + it("should clear to the left of cursor when given -1 as direction", () => { + assert.strictEqual(readline.clearLine(writable, -1), true); + assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning); + }); + + it("should clear to the right of cursor when given 1 as direction", () => { + assert.strictEqual(readline.clearLine(writable, 1), true); + assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd); + }); + + it("should clear whole line when given 0 as direction", () => { + assert.strictEqual(readline.clearLine(writable, 0), true); + assert.deepStrictEqual(writable.data, CSI.kClearLine); + }); + + it("should call callback after clearing line", (done) => { + const { mustCall } = createCallCheckCtx(done); + assert.strictEqual(readline.clearLine(writable, -1, mustCall()), true); + assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning); + }); + + it("should throw on an invalid callback", () => { + // Verify that clearLine() throws on invalid callback. + assert.throws(() => { + readline.clearLine(writable, 0, null); + }, /ERR_INVALID_ARG_TYPE/); + }); + + it("shouldn't throw on on null or undefined stream", (done) => { + const { mustCall } = createCallCheckCtx(done); + // Verify that clearLine() does not throw on null or undefined stream. + assert.strictEqual(readline.clearLine(null, 0), true); + assert.strictEqual(readline.clearLine(undefined, 0), true); + assert.strictEqual( + readline.clearLine( + null, + 0, + mustCall((err) => { + assert.strictEqual(err, null); + }), + ), + true, + ); + assert.strictEqual(readline.clearLine(undefined, 0, mustCall()), true); + }); +}); + +describe("readline.moveCursor()", () => { + it("shouldn't write when moveCursor(0, 0) is called", (done) => { + const { mustCall } = createCallCheckCtx(done); + // Nothing is written when moveCursor 0, 0 + [ + [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"], + ].forEach((set) => { + writable.data = ""; + assert.strictEqual(readline.moveCursor(writable, set[0], set[1]), true); + assert.deepStrictEqual(writable.data, set[2]); + writable.data = ""; + assert.strictEqual( + readline.moveCursor(writable, set[0], set[1], mustCall()), + true, + ); + assert.deepStrictEqual(writable.data, set[2]); + }); + }); + + it("should throw on invalid callback", () => { + // Verify that moveCursor() throws on invalid callback. + assert.throws(() => { + readline.moveCursor(writable, 1, 1, null); + }, /ERR_INVALID_ARG_TYPE/); + }); + + it("should not throw on null or undefined stream", (done) => { + const { mustCall } = createCallCheckCtx(done); + // Verify that moveCursor() does not throw on null or undefined stream. + assert.strictEqual(readline.moveCursor(null, 1, 1), true); + assert.strictEqual(readline.moveCursor(undefined, 1, 1), true); + assert.strictEqual( + readline.moveCursor( + null, + 1, + 1, + mustCall((err) => { + assert.strictEqual(err, null); + }), + ), + true, + ); + assert.strictEqual(readline.moveCursor(undefined, 1, 1, mustCall()), true); + }); +}); + +describe("readline.cursorTo()", () => { + beforeEach(() => { + writable.data = ""; + }); + + it("should not throw on undefined or null as stream", (done) => { + const { mustCall } = createCallCheckCtx(done); + // Undefined or null as stream should not throw. + assert.strictEqual(readline.cursorTo(null), true); + assert.strictEqual(readline.cursorTo(), true); + assert.strictEqual(readline.cursorTo(null, 1, 1, mustCall()), true); + assert.strictEqual( + readline.cursorTo( + undefined, + 1, + 1, + mustCall((err) => { + assert.strictEqual(err, null); + }), + ), + true, + ); + }); + + it("should not write if given invalid cursor position - [string, undefined]", () => { + assert.strictEqual(readline.cursorTo(writable, "a"), true); + assert.strictEqual(writable.data, ""); + }); + + it("should not write if given invalid cursor position - [string, string]", () => { + assert.strictEqual(readline.cursorTo(writable, "a", "b"), true); + assert.strictEqual(writable.data, ""); + }); + + it("should throw when x is not a number", () => { + assert.throws(() => readline.cursorTo(writable, "a", 1), { + name: "TypeError", + code: "ERR_INVALID_CURSOR_POS", + message: "Cannot set cursor row without setting its column", + }); + assert.strictEqual(writable.data, ""); + }); + + it("should write when given value cursor positions", (done) => { + const { mustCall } = createCallCheckCtx(done); + + assert.strictEqual(readline.cursorTo(writable, 1, "a"), true); + assert.strictEqual(writable.data, "\x1b[2G"); + + writable.data = ""; + assert.strictEqual(readline.cursorTo(writable, 1), true); + assert.strictEqual(writable.data, "\x1b[2G"); + + writable.data = ""; + assert.strictEqual(readline.cursorTo(writable, 1, 2), true); + assert.strictEqual(writable.data, "\x1b[3;2H"); + + writable.data = ""; + assert.strictEqual(readline.cursorTo(writable, 1, 2, mustCall()), true); + assert.strictEqual(writable.data, "\x1b[3;2H"); + + writable.data = ""; + assert.strictEqual(readline.cursorTo(writable, 1, mustCall()), true); + assert.strictEqual(writable.data, "\x1b[2G"); + }); + + it("should throw on invalid callback", () => { + // Verify that cursorTo() throws on invalid callback. + assert.throws(() => { + readline.cursorTo(writable, 1, 1, null); + }, /ERR_INVALID_ARG_TYPE/); + }); + + it("should throw if x or y is NaN", () => { + // Verify that cursorTo() throws if x or y is NaN. + assert.throws(() => { + readline.cursorTo(writable, NaN); + }, /ERR_INVALID_ARG_VALUE/); + + assert.throws(() => { + readline.cursorTo(writable, 1, NaN); + }, /ERR_INVALID_ARG_VALUE/); + + assert.throws(() => { + readline.cursorTo(writable, NaN, NaN); + }, /ERR_INVALID_ARG_VALUE/); + }); +}); + +describe("readline.emitKeyPressEvents()", () => { + // emitKeypressEvents is thoroughly tested in test-readline-keys.js. + // However, that test calls it implicitly. This is just a quick sanity check + // to verify that it works when called explicitly. + + const expectedSequence = ["f", "o", "o"]; + const expectedKeys = [ + { sequence: "f", name: "f", ctrl: false, meta: false, shift: false }, + { sequence: "o", name: "o", ctrl: false, meta: false, shift: false }, + { sequence: "o", name: "o", ctrl: false, meta: false, shift: false }, + ]; + + it("should emit the expected sequence when keypress listener added after called", () => { + const stream = new PassThrough(); + const sequence: any[] = []; + const keys: any[] = []; + + readline.emitKeypressEvents(stream); + stream.on("keypress", (s, k) => { + sequence.push(s); + keys.push(k); + }); + stream.write("foo"); + + assert.deepStrictEqual(sequence, expectedSequence); + assert.deepStrictEqual(keys, expectedKeys); + }); + + it("should emit the expected sequence when keypress listener added before called", () => { + const stream = new PassThrough(); + const sequence: any[] = []; + const keys: any[] = []; + + stream.on("keypress", (s, k) => { + sequence.push(s); + keys.push(k); + }); + readline.emitKeypressEvents(stream); + stream.write("foo"); + + assert.deepStrictEqual(sequence, expectedSequence); + assert.deepStrictEqual(keys, expectedKeys); + }); + + it("should allow keypress listeners to be removed and added again", () => { + const stream = new PassThrough(); + const sequence: any[] = []; + const keys: any[] = []; + const keypressListener = (s, k) => { + sequence.push(s); + keys.push(k); + }; + + stream.on("keypress", keypressListener); + readline.emitKeypressEvents(stream); + stream.removeListener("keypress", keypressListener); + stream.write("foo"); + + assert.deepStrictEqual(sequence, []); + assert.deepStrictEqual(keys, []); + + stream.on("keypress", keypressListener); + stream.write("foo"); + + assert.deepStrictEqual(sequence, expectedSequence); + assert.deepStrictEqual(keys, expectedKeys); + }); +}); + +describe("readline.Interface", () => { + it("should allow valid escapeCodeTimeout to be set", () => { + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + escapeCodeTimeout: 50, + }); + assert.strictEqual(rli.escapeCodeTimeout, 50); + rli.close(); + }); + + it("should throw on invalid escapeCodeTimeout", () => { + [null, {}, NaN, "50"].forEach((invalidInput) => { + assert.throws( + () => { + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + escapeCodeTimeout: invalidInput, + }); + rli.close(); + }, + { + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + }, + ); + }); + }); + + it("should create valid instances of readline.Interface", () => { + const input = new FakeInput(); + const rl = readline.Interface({ input }); + assert.ok(rl instanceof readline.Interface); + }); + + it("should call completer when input emits data", (done) => { + const { mustCall } = createCallCheckCtx(done); + const fi = new FakeInput(); + const rli = new readline.Interface( + fi, + fi, + mustCall((line) => [[], line]), + true, + ); + + assert.ok(rli instanceof readline.Interface); + fi.emit("data", "a\t"); + rli.close(); + }); + + it("should allow crlfDelay to be set", () => { + [undefined, 50, 0, 100.5, 5000].forEach((crlfDelay) => { + const [rli] = getInterface({ crlfDelay }); + assert.strictEqual(rli.crlfDelay, Math.max(crlfDelay || 100, 100)); + rli.close(); + }); + }); + + it("should throw if completer is not a function or is 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", + }, + ); + }); + }); + + it("should throw if history is not an array", () => { + ["not an array", 123, 123, {}, true, Symbol(), null].forEach((history) => { + assert.throws( + () => { + readline.createInterface({ + input, + history, + }); + }, + { + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + }, + ); + }); + }); + + it("should throw if historySize is not a positive number", () => { + ["not a number", -1, NaN, {}, true, Symbol(), null].forEach( + (historySize) => { + assert.throws( + () => { + readline.createInterface({ + input, + historySize, + }); + }, + { + // TODO: Revert to Range error when properly implemented errors with multiple bases + // name: "RangeError", + name: "TypeError", + code: "ERR_INVALID_ARG_VALUE", + }, + ); + }, + ); + }); + + it("should throw on invalid tabSize", () => { + // 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 + it("should not emit line when only a single character sent with no newline", (done) => { + const { mustNotCall } = createCallCheckCtx(done); + const fi = new FakeInput(); + const rli = new readline.Interface(fi, {}); + rli.on("line", mustNotCall()); + fi.emit("data", "a"); + rli.close(); + }); + + it("should treat \\r like \\n when alone", (done) => { + const { mustCall } = createCallCheckCtx(done); + // 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", + 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 + it("should output blank line when \\r at start of input", (done) => { + const { mustCall } = createCallCheckCtx(done); + const [rli, fi] = getInterface({ terminal: true }); + const expectedLines = ["", "foo"]; + rli.on( + "line", + 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 + it("should not include \\t in input when there is a completer function", (done) => { + const { mustCall } = createCallCheckCtx(done); + const completer = (line) => [[], line]; + const [rli, fi] = getInterface({ terminal: true, completer }); + rli.on( + "line", + 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 + it("should treat \\t as an ordinary character when there is no completer function", (done) => { + const { mustCall } = createCallCheckCtx(done); + const [rli, fi] = getInterface({ terminal: true }); + rli.on( + "line", + 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 + it("should emit history event when adding history lines", (done) => { + const { mustCall } = createCallCheckCtx(done); + const [rli, fi] = getInterface({ terminal: true }); + const expectedLines = ["foo", "bar", "baz", "bat"]; + rli.on( + "history", + 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 + it("should not alter the line being processed when history is altered", (done) => { + const { mustCall } = createCallCheckCtx(done); + const [rli, fi] = getInterface({ terminal: true }); + const expectedLine = "foo"; + rli.on( + "history", + mustCall((history) => { + assert.strictEqual(history[0], expectedLine); + history.shift(); + }), + ); + rli.on( + "line", + 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` + it("should remove duplicate lines from history when 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` + it("should not remove duplicate lines from history when 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. + it("should not fail if keypress throws", () => { + const [rli, fi] = getInterface({ terminal: true }); + const keys = [] as string[]; + const err = new Error("bad thing happened"); + fi.on("keypress", (key: string) => { + keys.push(key); + if (key === "X") { + throw err; + } + }); + assert.throws( + () => fi.emit("data", "fooX"), + (e) => { + console.log("ERRROR!", e); + assert.strictEqual(e, err); + return true; + }, + ); + fi.emit("data", "bar"); + assert.strictEqual(keys.join(""), "fooXbar"); + rli.close(); + }); + + // History is bound + it("should bind history", () => { + 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 + it("should handle 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 + it("should handle multi-line questions", () => { + 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(); + }); + + it("should handle beginning and end of line", () => { + // 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(); + }); + + it("should handle back and forward one character", () => { + // 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 + it("should handle going back and forward one astral character", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "đť"); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // Two astral characters left + it("should handle two astral characters left", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "đđť"); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // Two astral characters right + it("should handle two astral characters right", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "đťđ"); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + it("should handle wordLeft and wordRight", () => { + // `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` + it("should handle deleteWordLeft", (done) => { + const { mustCall } = createCallCheckCtx(done); + [ + { 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", + 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", + mustCall((line) => { + assert.strictEqual(line, "the quick brown fox"); + }), + ); + fi.emit("keypress", ".", deleteWordLeftKey); + fi.emit("data", "\n"); + rli.close(); + }); + }); + + // `deleteWordRight` + it("should handle deleteWordRight", (done) => { + const { mustCall } = createCallCheckCtx(done); + [ + { 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", + 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", + mustCall((line) => { + assert.strictEqual(line, "the quick brown fox"); + }), + ); + fi.emit("keypress", ".", deleteWordRightKey); + fi.emit("data", "\n"); + rli.close(); + }); + }); + + // deleteLeft + it("should handle deleteLeft", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "the quick brown fo"); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // deleteLeft astral character + it("should handle deleteLeft astral character", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, ""); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // deleteRight + it("should handle deleteRight", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "he quick brown fox"); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // deleteRight astral character + it("should handle deleteRight of astral characters", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, ""); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // deleteLineLeft + it("should handle deleteLineLeft", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, ""); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // deleteLineRight + it("should handle deleteLineRight", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, ""); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + // yank + it("should handle yank", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "the quick brown fox"); + }), + ); + + fi.emit("data", "\n"); + rli.close(); + }); + + // yank pop + it("should handle yank pop", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "he quick brown foxthe "); + }), + ); + + fi.emit("data", "\n"); + rli.close(); + }); + + // Close readline interface + it("Should close readline interface", () => { + const [rli, fi] = getInterface({ terminal: true, prompt: "" }); + fi.emit("keypress", ".", { ctrl: true, name: "c" }); + assert.ok(rli.closed); + }); + + // Multi-line input cursor position + it("should handle multi-line input cursors", () => { + 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 + it("should handle 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. + it("should use 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 + it("should handle 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 + it("should undo and 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 + it("should clear the whole screen", (done) => { + const { mustCall } = createCallCheckCtx(done); + 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", + mustCall((line) => { + assert.strictEqual(line, "line 3"); + }), + ); + fi.emit("data", "\n"); + rli.close(); + }); + + it("should treat wide characters 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', mustCall((line) => { + // assert.strictEqual(line, 'asdf'); + // })); + // fi.emit('data', 'asdf\n'); + // } + + // // Sending a blank line + // { + // const [rli, fi] = getInterface({ terminal }); + // rli.on('line', 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', 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', 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', mustCall((line) => { + // assert.strictEqual(line, expectedLines.shift()); + // }, expectedLines.length - 1)); + // rli.on('close', 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', mustCall((line) => { + // assert.strictEqual(line, 'asdf'); + // })); + // fi.emit('data', 'asdf\n'); + // rli.close(); + // } + + // // Calling the question callback + // { + // const [rli] = getInterface({ terminal }); + // rli.question('foo?', 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 }, mustCall((answer) => { + // assert.strictEqual(answer, 'bar'); + // })); + // rli.write('bar\n'); + // rli.close(); + // } + + // // Calling the question multiple times + // { + // const [rli] = getInterface({ terminal }); + // rli.question('foo?', mustCall((answer) => { + // assert.strictEqual(answer, 'baz'); + // })); + // rli.question('bar?', 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(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(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', mustCall((line) => { + // assert.strictEqual(line, 'bar'); + // })); + // rli.question('hello?', { signal }, 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', mustCall((line) => { + // assert.strictEqual(line, 'bar'); + // })); + // question('hello?', { signal }) + // .then(mustNotCall()) + // .catch(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', mustNotCall()); + // rli.question('hello?', { signal }, 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', mustNotCall()); + // rli.pause(); + // question('hello?', { signal }) + // .then(mustNotCall()) + // .catch(mustCall((error) => { + // assert.strictEqual(error.name, 'AbortError'); + // })); + // rli.close(); + // } + + // // Call question after close + // { + // const [rli, fi] = getInterface({ terminal }); + // rli.question('What\'s your name?', mustCall((name) => { + // assert.strictEqual(name, 'Node.js'); + // rli.close(); + // assert.throws(() => { + // rli.question('How are you?', 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(mustCall((name) => { + // assert.strictEqual(name, 'Node.js'); + // rli.close(); + // question('How are you?') + // .then(mustNotCall(), 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', 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: mustCall((chunk, enc, cb) => { + // assert.strictEqual(chunk.toString(), expected.shift()); + // cb(); + // rl.close(); + // }, expected.length) + // }); + + // const rl = readline.createInterface({ + // input: new Readable({ read: 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(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(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', 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', 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(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: 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', 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', 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' + // }); + // } +}); + +describe("readline.createInterface()", () => { + it("should emit line when input ends line", (done) => { + const createDone = createDoneDotAll(done); + const lineDone = createDone(2000); + const { mustCall } = createCallCheckCtx(createDone(2000)); + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input, + }); + + rl.on( + "line", + mustCall((data) => { + assert.strictEqual(data, "abc"); + lineDone(); + }), + ); + + input.end("abc"); + }); + + it("should not emit line when input ends without newline", (done) => { + const { mustNotCall } = createCallCheckCtx(done); + + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input, + }); + + rl.on("line", mustNotCall("must not be called before newline")); + input.write("abc"); + }); + + it("should read line by line", (done) => { + const createDone = createDoneDotAll(done); + const { mustCall } = createCallCheckCtx(createDone(3000)); + const lineDone = createDone(2000); + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input, + }); + + rl.on( + "line", + mustCall((data) => { + assert.strictEqual(data, "abc"); + lineDone(); + }), + ); + + input.write("abc\n"); + }); + + it("should respond to home and end sequences for common pttys ", () => { + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input, + }); + + rl.write("foo"); + assert.strictEqual(rl.cursor, 3); + + const key = { + xterm: { + home: ["\x1b[H", { ctrl: true, name: "a" }], + end: ["\x1b[F", { ctrl: true, name: "e" }], + }, + gnome: { + home: ["\x1bOH", { ctrl: true, name: "a" }], + end: ["\x1bOF", { ctrl: true, name: "e" }], + }, + rxvt: { + home: ["\x1b[7", { ctrl: true, name: "a" }], + end: ["\x1b[8", { ctrl: true, name: "e" }], + }, + putty: { + home: ["\x1b[1~", { ctrl: true, name: "a" }], + end: ["\x1b[>~", { ctrl: true, name: "e" }], + }, + }; + + [key.xterm, key.gnome, key.rxvt, key.putty].forEach((key) => { + rl.write.apply(rl, key.home); + assert.strictEqual(rl.cursor, 0); + rl.write.apply(rl, key.end); + assert.strictEqual(rl.cursor, 3); + }); + }); + + it("should allow for cursor movement with meta-f and meta-b", () => { + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input, + }); + + const key = { + xterm: { + home: ["\x1b[H", { ctrl: true, name: "a" }], + metab: ["\x1bb", { meta: true, name: "b" }], + metaf: ["\x1bf", { meta: true, name: "f" }], + }, + }; + + rl.write("foo bar.hop/zoo"); + rl.write.apply(rl, key.xterm.home); + [ + { cursor: 4, key: key.xterm.metaf }, + { cursor: 7, key: key.xterm.metaf }, + { cursor: 8, key: key.xterm.metaf }, + { cursor: 11, key: key.xterm.metaf }, + { cursor: 12, key: key.xterm.metaf }, + { cursor: 15, key: key.xterm.metaf }, + { cursor: 12, key: key.xterm.metab }, + { cursor: 11, key: key.xterm.metab }, + { cursor: 8, key: key.xterm.metab }, + { cursor: 7, key: key.xterm.metab }, + { cursor: 4, key: key.xterm.metab }, + { cursor: 0, key: key.xterm.metab }, + ].forEach(function (action) { + rl.write.apply(rl, action.key); + assert.strictEqual(rl.cursor, action.cursor); + }); + }); + + it("should properly allow for cursor movement with meta-d", () => { + const input = new PassThrough(); + const rl = readline.createInterface({ + terminal: true, + input: input, + }); + + const key = { + xterm: { + home: ["\x1b[H", { ctrl: true, name: "a" }], + metad: ["\x1bd", { meta: true, name: "d" }], + }, + }; + + rl.write("foo bar.hop/zoo"); + rl.write.apply(rl, key.xterm.home); + ["bar.hop/zoo", ".hop/zoo", "hop/zoo", "/zoo", "zoo", ""].forEach(function ( + expectedLine, + ) { + rl.write.apply(rl, key.xterm.metad); + assert.strictEqual(rl.cursor, 0); + assert.strictEqual(rl.line, expectedLine); + }); + }); + + // TODO: Actual pseudo-tty test + // it("should operate correctly when process.env.DUMB is set", () => { + // process.env.TERM = "dumb"; + // const rl = readline.createInterface({ + // input: process.stdin, + // output: process.stdout, + // }); + // rl.write("text"); + // rl.write(null, { ctrl: true, name: "u" }); + // rl.write(null, { name: "return" }); + // rl.write("text"); + // rl.write(null, { name: "backspace" }); + // rl.write(null, { name: "escape" }); + // rl.write(null, { name: "enter" }); + // rl.write("text"); + // rl.write(null, { ctrl: true, name: "c" }); + // }); +}); diff --git a/test/bun.js/readline_promises.node.test.ts b/test/bun.js/readline_promises.node.test.ts new file mode 100644 index 000000000..eadd289e3 --- /dev/null +++ b/test/bun.js/readline_promises.node.test.ts @@ -0,0 +1,55 @@ +import { describe, it } from "bun:test"; +import readlinePromises from "node:readline/promises"; +import { EventEmitter } from "node:events"; +import { + createDoneDotAll, + createCallCheckCtx, + assert, +} from "./node-test-helpers"; + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +class FakeInput extends EventEmitter { + output = ""; + resume() {} + pause() {} + write(data) { + this.output += data; + } + end() {} + reset() { + this.output = ""; + } +} + +// ---------------------------------------------------------------------------- +// Tests +// ---------------------------------------------------------------------------- + +describe("readline/promises.createInterface()", () => { + it("should throw an error when failed completion", (done) => { + const createDone = createDoneDotAll(done); + const { mustCall, mustNotCall } = createCallCheckCtx(createDone()); + + const fi = new FakeInput(); + const rli = new readlinePromises.Interface({ + input: fi, + output: fi, + terminal: true, + completer: mustCall(() => Promise.reject(new Error("message"))), + }); + + rli.on("line", mustNotCall()); + fi.emit("data", "\t"); + const outCheckDone = createDone(); + process.nextTick(() => { + console.log("output", fi.output); + assert.match(fi.output, /^Tab completion error/); + fi.reset(); + outCheckDone(); + }); + rli.close(); + }); +}); |