diff --git a/lib/utils/display.js b/lib/utils/display.js index 67a3b98c0417a..a682b4dbb2fb2 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -347,20 +347,37 @@ class Display { case input.KEYS.end: log.resume() + // For silent prompts (like password), add newline to preserve output + if (args[0]?.[0]?.silent) { + output.standard('') + } output.flush() this.#progress.resume() break case input.KEYS.read: { - // The convention when calling input.read is to pass in a single fn that returns - // the promise to await. resolve and reject are provided by proc-log - const [res, rej, p] = args - return input.start(() => p() - .then(res) - .catch(rej) - // Any call to procLog.input.read will render a prompt to the user, so we always - // add a single newline of output to stdout to move the cursor to the next line - .finally(() => output.standard(''))) + // The convention when calling input.read is to pass in a fn that returns + // the promise to await. resolve and reject are provided by proc-log. + // The last argument are the options passed to read which includes the silent flag + const [res, rej, p, options] = args + + // Use sequential input management to avoid race condition which causes issues + // with spinner and adding newlines + process.emit('input', 'start') + + return p() + .then((result) => { + // User hits enter, process end event and return input + process.emit('input', 'end', options) + res(result) + return result + }) + .catch((error) => { + // User hits ctrl+c, add newline to preserve output + output.standard('') + process.emit('input', 'end') + rej(error) + }) } } }) diff --git a/lib/utils/read-user-info.js b/lib/utils/read-user-info.js index a9a50f8263ff6..d20b54526e90e 100644 --- a/lib/utils/read-user-info.js +++ b/lib/utils/read-user-info.js @@ -11,7 +11,8 @@ const passwordPrompt = 'npm password: ' const usernamePrompt = 'npm username: ' const emailPrompt = 'email (this IS public): ' -const read = (...args) => input.read(() => _read(...args)) +// Pass options through so we can differentiate between regular and silent prompts +const read = (...args) => input.read(() => _read(...args), args) function readOTP (msg = otpPrompt, otp, isRetry) { if (isRetry && otp && /^[\d ]+$|^[A-Fa-f0-9]{64,64}$/.test(otp)) { diff --git a/tap-snapshots/test/lib/commands/init.js.test.cjs b/tap-snapshots/test/lib/commands/init.js.test.cjs index eae04d77d2e82..821193a55e1a9 100644 --- a/tap-snapshots/test/lib/commands/init.js.test.cjs +++ b/tap-snapshots/test/lib/commands/init.js.test.cjs @@ -20,6 +20,5 @@ Press ^C at any time to quit. exports[`test/lib/commands/init.js TAP workspaces no args -- yes > should print helper info 1`] = ` - added 1 package in {TIME} ` diff --git a/tap-snapshots/test/lib/utils/open-url.js.test.cjs b/tap-snapshots/test/lib/utils/open-url.js.test.cjs index fa256ba131447..6ef050d66dfd7 100644 --- a/tap-snapshots/test/lib/utils/open-url.js.test.cjs +++ b/tap-snapshots/test/lib/utils/open-url.js.test.cjs @@ -25,7 +25,6 @@ https://www.npmjs.com exports[`test/lib/utils/open-url.js TAP open url prompt does not error when opener can not find command > Outputs extra Browser unavailable message and url 1`] = ` npm home: https://www.npmjs.com - Browser unavailable. Please open the URL manually: https://www.npmjs.com ` diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index 78bffa0221d03..74b76a8e56240 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -197,3 +197,46 @@ t.test('Display.clean', async (t) => { clearOutput() } }) + +t.test('prompt functionality', async t => { + t.test('regular prompt completion works', async t => { + const { input } = await mockDisplay(t) + + const result = await input.read(() => Promise.resolve('user-input')) + + t.equal(result, 'user-input', 'should return the input result') + }) + + t.test('silent prompt completion works', async t => { + const { input } = await mockDisplay(t) + + const result = await input.read( + () => Promise.resolve('secret-password'), + { silent: true } + ) + + t.equal(result, 'secret-password', 'should return the input result for silent prompts') + }) + + t.test('metadata is correctly passed through', async t => { + const { input } = await mockDisplay(t) + + await input.read( + () => Promise.resolve('result1'), + { silent: false } + ) + t.pass('should handle silent false option') + + await input.read( + () => Promise.resolve('result2'), + {} + ) + t.pass('should handle empty options') + + await input.read( + () => Promise.resolve('result3'), + { silent: true } + ) + t.pass('should handle silent true option') + }) +}) diff --git a/test/lib/utils/read-user-info.js b/test/lib/utils/read-user-info.js index 35628f7f2faac..74463175f5e8e 100644 --- a/test/lib/utils/read-user-info.js +++ b/test/lib/utils/read-user-info.js @@ -118,3 +118,35 @@ t.test('email - invalid warns and retries', async (t) => { t.equal(result, 'foo@bar.baz', 'received the email') t.equal(logMsg, 'invalid email') }) + +t.test('read-user-info integration works', async (t) => { + t.teardown(() => { + readResult = null + readOpts = null + }) + + readResult = 'regular-input' + const username = await readUserInfo.username('Username: ') + t.equal(username, 'regular-input', 'should return username from regular prompt') + t.notOk(readOpts.silent, 'username prompt should not set silent') + + readResult = 'secret-password' + const password = await readUserInfo.password('Password: ') + t.equal(password, 'secret-password', 'should return password from silent prompt') + t.match(readOpts, { silent: true }, 'password prompt should set silent: true') +}) + +t.test('silent metadata is passed correctly by read-user-info', async (t) => { + t.teardown(() => { + readResult = null + readOpts = null + }) + + readResult = 'username' + await readUserInfo.username('Username: ') + t.notOk(readOpts?.silent, 'username prompt should not set silent') + + readResult = 'password' + await readUserInfo.password('Password: ') + t.equal(readOpts?.silent, true, 'password prompt should set silent: true') +})