aboutsummaryrefslogtreecommitdiff
path: root/test/bun.js
diff options
context:
space:
mode:
authorGravatar Derrick Farris <mr.dcfarris@gmail.com> 2023-01-08 03:49:49 -0600
committerGravatar GitHub <noreply@github.com> 2023-01-08 01:49:49 -0800
commit94409770dece8bb9dfc23f4bdc2f240836035d87 (patch)
tree4cc627eb67c476871e84141a6c7a583e29e98309 /test/bun.js
parentc505f172b84f5359aa186513f3ef7d6394bfc7b2 (diff)
downloadbun-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.js156
-rw-r--r--test/bun.js/node-test-helpers.ts198
-rw-r--r--test/bun.js/readline.node.test.ts2018
-rw-r--r--test/bun.js/readline_promises.node.test.ts55
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();
+ });
+});