diff options
Diffstat (limited to 'test/js/node/readline/readline.node.test.ts')
-rw-r--r-- | test/js/node/readline/readline.node.test.ts | 2001 |
1 files changed, 2001 insertions, 0 deletions
diff --git a/test/js/node/readline/readline.node.test.ts b/test/js/node/readline/readline.node.test.ts new file mode 100644 index 000000000..7dfa51ac8 --- /dev/null +++ b/test/js/node/readline/readline.node.test.ts @@ -0,0 +1,2001 @@ +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-harness"; + +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" }); + // }); +}); |