mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
* 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
2019 lines
62 KiB
TypeScript
2019 lines
62 KiB
TypeScript
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" });
|
|
// });
|
|
});
|