diff --git a/src/js/node/readline.ts b/src/js/node/readline.ts index dc1e52d686f1..63de7c2ed881 100644 --- a/src/js/node/readline.ts +++ b/src/js/node/readline.ts @@ -2699,28 +2699,26 @@ var PromisesInterface = class Interface extends _Interface { super(input, output, completer, terminal); } question(query, options = kEmptyObject) { - var signal = options?.signal; - if (signal) { - validateAbortSignal(signal, "options.signal"); - if (signal.aborted) { - return PromiseReject($makeAbortError(undefined, { cause: signal.reason })); + return new Promise((resolve, reject) => { + var cb = resolve; + var signal = options?.signal; + if (signal) { + validateAbortSignal(signal, "options.signal"); + if (signal.aborted) { + return reject($makeAbortError(undefined, { cause: signal.reason })); + } + var onAbort = () => { + this[kQuestionCancel](); + reject($makeAbortError(undefined, { cause: signal.reason })); + }; + signal.addEventListener("abort", onAbort, { once: true }); + cb = answer => { + signal.removeEventListener("abort", onAbort); + resolve(answer); + }; } - } - const { promise, resolve, reject } = $newPromiseCapability(Promise); - var cb = resolve; - if (options?.signal) { - var onAbort = () => { - this[kQuestionCancel](); - reject($makeAbortError(undefined, { cause: signal.reason })); - }; - signal.addEventListener("abort", onAbort, { once: true }); - cb = answer => { - signal.removeEventListener("abort", onAbort); - resolve(answer); - }; - } - this[kQuestion](query, cb); - return promise; + this[kQuestion](query, cb); + }); } }; diff --git a/test/js/node/readline/readline_promises.node.test.ts b/test/js/node/readline/readline_promises.node.test.ts index 97f9d6f3af27..cb6f09bd4919 100644 --- a/test/js/node/readline/readline_promises.node.test.ts +++ b/test/js/node/readline/readline_promises.node.test.ts @@ -1,6 +1,7 @@ import { createTest } from "node-harness"; import { EventEmitter } from "node:events"; import readlinePromises from "node:readline/promises"; +import { PassThrough } from "node:stream"; const { describe, it, expect, createDoneDotAll, createCallCheckCtx, assert } = createTest(import.meta.path); // ---------------------------------------------------------------------------- @@ -93,3 +94,51 @@ describe("readline/promises.createInterface()", () => { assert.strictEqual(rl.closed, true); }); }); + +describe("readline/promises.Interface.question()", () => { + it("returns a rejected promise (not a sync throw) on a closed interface", async () => { + const rl = readlinePromises.createInterface({ input: new PassThrough() }); + rl.close(); + const result = rl.question("q? "); + expect(result).toBeInstanceOf(Promise); + const err = await result.then( + () => null, + e => e, + ); + expect({ name: err?.name, code: err?.code }).toEqual({ name: "Error", code: "ERR_USE_AFTER_CLOSE" }); + }); + + it("returns a rejected promise (not a sync throw) for an invalid options.signal", async () => { + const rl = readlinePromises.createInterface({ input: new PassThrough() }); + try { + const result = rl.question("q? ", { signal: 42 as any }); + expect(result).toBeInstanceOf(Promise); + const err = await result.then( + () => null, + e => e, + ); + expect({ name: err?.name, code: err?.code }).toEqual({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE" }); + } finally { + rl.close(); + } + }); + + it("returns a rejected promise for an already-aborted signal", async () => { + const rl = readlinePromises.createInterface({ input: new PassThrough() }); + try { + const result = rl.question("q? ", { signal: AbortSignal.abort("why") }); + expect(result).toBeInstanceOf(Promise); + const err = await result.then( + () => null, + e => e, + ); + expect({ name: err?.name, code: err?.code, cause: err?.cause }).toEqual({ + name: "AbortError", + code: "ABORT_ERR", + cause: "why", + }); + } finally { + rl.close(); + } + }); +});