Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions lib/utils/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
})
Expand Down
3 changes: 2 additions & 1 deletion lib/utils/read-user-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
1 change: 0 additions & 1 deletion tap-snapshots/test/lib/commands/init.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
`
1 change: 0 additions & 1 deletion tap-snapshots/test/lib/utils/open-url.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
`
Expand Down
43 changes: 43 additions & 0 deletions test/lib/utils/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
32 changes: 32 additions & 0 deletions test/lib/utils/read-user-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
Loading