diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec353c7b1..2600df973 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,6 +107,7 @@ jobs: - https://github.com/salesforcecli/plugin-org - https://github.com/salesforcecli/plugin-schema - https://github.com/salesforcecli/plugin-user + - https://github.com/salesforcecli/plugin-settings with: packageName: '@oclif/core' externalProjectGitUrl: ${{ matrix.externalProjectGitUrl }} @@ -153,7 +154,7 @@ jobs: with: repo: oclif/plugin-plugins os: ${{ matrix.os }} - command: 'yarn test:integration' + command: yarn test:integration --retries 3 # plugin-plugins integration tests depend on sf being installed globally other-setup: npm install -g @salesforce/cli@nightly plugin-update-integration: @@ -166,4 +167,4 @@ jobs: with: repo: oclif/plugin-update os: ${{ matrix.os }} - command: 'yarn test:integration:sf' + command: yarn test:integration:sf --retries 3 diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts index afed56bc4..84f25dc21 100644 --- a/src/cli-ux/prompt.ts +++ b/src/cli-ux/prompt.ts @@ -1,5 +1,4 @@ import chalk from 'chalk' -import readline from 'node:readline' import * as Errors from '../errors' import {config} from './config' @@ -27,37 +26,26 @@ interface IPromptConfig { function normal(options: IPromptConfig, retries = 100): Promise { if (retries < 0) throw new Error('no input') - const ac = new AbortController() - const {signal} = ac - return new Promise((resolve, reject) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) - let timeout: NodeJS.Timeout - // Only set the timeout if the input is a TTY - if (options.timeout && options.isTTY) { - timeout = setTimeout(() => ac.abort(), options.timeout) - signal.addEventListener( - 'abort', - () => { - rl.close() - clearTimeout(timeout) - reject(new Error('Prompt timeout')) - }, - {once: true}, - ) + let timer: NodeJS.Timeout + if (options.timeout) { + timer = setTimeout(() => { + process.stdin.pause() + reject(new Error('Prompt timeout')) + }, options.timeout) + timer.unref() } - rl.question(options.prompt, {signal}, (answer) => { - rl.close() - const data = answer.trim() + process.stdin.setEncoding('utf8') + process.stderr.write(options.prompt) + process.stdin.resume() + process.stdin.once('data', (b) => { + if (timer) clearTimeout(timer) + process.stdin.pause() + const data: string = (typeof b === 'string' ? b : b.toString()).trim() if (!options.default && options.required && data === '') { - clearTimeout(timeout) resolve(normal(options, retries - 1)) } else { - clearTimeout(timeout) resolve(data || (options.default as string)) } }) diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts index 1afe3922b..7f34e83d8 100644 --- a/test/cli-ux/prompt.test.ts +++ b/test/cli-ux/prompt.test.ts @@ -1,76 +1,73 @@ -import {expect} from 'chai' -import readline from 'node:readline' -import {SinonSandbox, createSandbox} from 'sinon' +import * as chai from 'chai' + +const {expect} = chai import {ux} from '../../src/cli-ux' +import {fancy} from './fancy' describe('prompt', () => { - let sandbox: SinonSandbox - - function stubReadline(answers: string[]) { - let callCount = 0 - sandbox.stub(readline, 'createInterface').returns({ - // @ts-expect-error because we're stubbing - async question(_message, opts, cb) { - callCount += 1 - cb(answers[callCount - 1]) - }, - close() {}, + fancy + .stdout() + .stderr() + .end('requires input', async () => { + const promptPromise = ux.prompt('Require input?') + process.stdin.emit('data', '') + process.stdin.emit('data', 'answer') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal('answer') }) - } - - beforeEach(() => { - sandbox = createSandbox() - }) - - afterEach(() => { - sandbox.restore() - }) - - it('should require input', async () => { - stubReadline(['', '', 'answer']) - const answer = await ux.prompt('Require input?') - expect(answer).to.equal('answer') - }) - - it('should not require input if required = false', async () => { - stubReadline(['']) - const answer = await ux.prompt('Require input?', {required: false}) - expect(answer).to.equal('') - }) - it('should use default input', async () => { - stubReadline(['']) - const answer = await ux.prompt('Require input?', {default: 'default'}) - expect(answer).to.equal('default') - }) + fancy + .stdout() + .stderr() + .stdin('y') + .end('confirm', async () => { + const promptPromise = ux.confirm('yes/no?') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal(true) + }) - it('should timeout after provided timeout', async () => { - stubReadline(['']) - sandbox.stub(process, 'stdin').value({isTTY: true}) - try { - await ux.prompt('Require input?', {timeout: 10}) - expect.fail('should have thrown') - } catch (error: any) { - expect(error.message).to.equal('Prompt timeout') - } - }) + fancy + .stdout() + .stderr() + .stdin('n') + .end('confirm', async () => { + const promptPromise = ux.confirm('yes/no?') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal(false) + }) - it('should confirm with y', async () => { - stubReadline(['y']) - const answer = await ux.confirm('yes/no?') - expect(answer).to.equal(true) - }) + fancy + .stdout() + .stderr() + .stdin('x') + .end('gets anykey', async () => { + const promptPromise = ux.anykey() + const answer = await promptPromise + await ux.done() + expect(answer).to.equal('x') + }) - it('should confirm with n', async () => { - stubReadline(['n']) - const answer = await ux.confirm('yes/no?') - expect(answer).to.equal(false) - }) + fancy + .stdout() + .stderr() + .end('does not require input', async () => { + const promptPromise = ux.prompt('Require input?', { + required: false, + }) + process.stdin.emit('data', '') + const answer = await promptPromise + await ux.done() + expect(answer).to.equal('') + }) - it('should get anykey', async () => { - stubReadline(['x']) - const answer = await ux.anykey() - expect(answer).to.equal('x') - }) + fancy + .stdout() + .stderr() + .it('timeouts with no input', async () => { + await expect(ux.prompt('Require input?', {timeout: 1})).to.eventually.be.rejectedWith('Prompt timeout') + }) })