From ee725e980d10d91e28a1db635ca6223f108b7e1f Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Tue, 7 Oct 2025 09:36:09 -0500 Subject: [PATCH] refactor: convert more code to use new inquirer --- .../lib/command/command-util.test.ts | 46 +++++++---------- src/__tests__/lib/command/select.test.ts | 49 +++++++++---------- .../lib/command/util/history.test.ts | 44 +++++++++-------- src/lib/command/command-util.ts | 17 +++---- src/lib/command/select.ts | 8 ++- src/lib/command/util/history.ts | 16 ++---- 6 files changed, 79 insertions(+), 101 deletions(-) diff --git a/src/__tests__/lib/command/command-util.test.ts b/src/__tests__/lib/command/command-util.test.ts index edd07190..c3a05837 100644 --- a/src/__tests__/lib/command/command-util.test.ts +++ b/src/__tests__/lib/command/command-util.test.ts @@ -1,17 +1,14 @@ import { jest } from '@jest/globals' -import inquirer from 'inquirer' +import type { sort } from '../../../lib/command/output.js' +import type { ListDataFunction, Sorting } from '../../../lib/command/io-defs.js' +import { stringInput } from '../../../lib/user-query.js' +import type { SimpleType } from '../../test-lib/simple-type.js' -import { sort } from '../../../lib/command/output.js' -import { ListDataFunction, Sorting } from '../../../lib/command/io-defs.js' -import { SimpleType } from '../../test-lib/simple-type.js' - -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - }, +const stringInputMock = jest.fn() +jest.unstable_mockModule('../../../lib/user-query.js', () => ({ + stringInput: stringInputMock, })) const sortMock = jest.fn() @@ -162,55 +159,46 @@ describe('convertToId', () => { describe('stringGetIdFromUser', () => { it('accepts id input from user', async () => { - promptMock.mockResolvedValue({ itemIdOrIndex: 'string-id-a' }) + stringInputMock.mockResolvedValue('string-id-a') const chosenId = await stringGetIdFromUser(config, list) expect(chosenId).toBe('string-id-a') - expect(promptMock).toHaveBeenCalledExactlyOnceWith({ - type: 'input', name: 'itemIdOrIndex', - message: 'Enter id or index', validate: expect.anything(), - }) - const validateFunction = (promptMock.mock.calls[0][0] as { validate: (input: string) => true | string }).validate + expect(stringInputMock).toHaveBeenCalledExactlyOnceWith('Enter id or index', { validate: expect.anything() }) + const validateFunction = (stringInputMock.mock.calls[0][1] as { validate: (input: string) => true | string }).validate expect(validateFunction('string-id-a')).toBe(true) }) it('validation returns error when unable to convert', async () => { - promptMock.mockResolvedValue({ itemIdOrIndex: 'string-id-a' }) + stringInputMock.mockResolvedValue('string-id-a') const chosenId = await stringGetIdFromUser(config, list) expect(chosenId).toBe('string-id-a') - expect(promptMock).toHaveBeenCalledExactlyOnceWith({ - type: 'input', name: 'itemIdOrIndex', - message: 'Enter id or index', validate: expect.anything(), - }) - const validateFunction = (promptMock.mock.calls[0][0] as { validate: (input: string) => true | string }).validate + expect(stringInputMock).toHaveBeenCalledExactlyOnceWith('Enter id or index', { validate: expect.anything() }) + const validateFunction = (stringInputMock.mock.calls[0][1] as { validate: (input: string) => true | string }).validate expect(validateFunction('invalid-id')).toBe('Invalid id or index "invalid-id". Please enter an index or valid id.') }) it('throws error when unable to convert entered value to a valid id', async () => { - promptMock.mockResolvedValue({ itemIdOrIndex: 'invalid-id' }) + stringInputMock.mockResolvedValue('invalid-id') await expect(stringGetIdFromUser(config, list)).rejects.toThrow('unable to convert invalid-id to id') }) it('handles non-default prompt', async () => { - promptMock.mockResolvedValue({ itemIdOrIndex: 'string-id-a' }) + stringInputMock.mockResolvedValue('string-id-a') const chosenId = await stringGetIdFromUser(config, list, 'give me an id') expect(chosenId).toBe('string-id-a') - expect(promptMock).toHaveBeenCalledExactlyOnceWith({ - type: 'input', name: 'itemIdOrIndex', - message: 'give me an id', validate: expect.anything(), - }) - const validateFunction = (promptMock.mock.calls[0][0] as { validate: (input: string) => true | string }).validate + expect(stringInputMock).toHaveBeenCalledExactlyOnceWith('give me an id', { validate: expect.anything() }) + const validateFunction = (stringInputMock.mock.calls[0][1] as { validate: (input: string) => true | string }).validate expect(validateFunction('string-id-a')).toBe(true) }) diff --git a/src/__tests__/lib/command/select.test.ts b/src/__tests__/lib/command/select.test.ts index f673abc8..db617445 100644 --- a/src/__tests__/lib/command/select.test.ts +++ b/src/__tests__/lib/command/select.test.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' -import inquirer from 'inquirer' +import type { select } from '@inquirer/prompts' import log4js from 'log4js' import type { @@ -25,13 +25,12 @@ import type { import type { SimpleType } from '../../test-lib/simple-type.js' -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - }, +const selectMock = jest.fn() +jest.unstable_mockModule('@inquirer/prompts', () => ({ + select: selectMock, })) + const resetManagedConfigKeyMock = jest.fn() const setConfigKeyMock = jest.fn() jest.unstable_mockModule('../../../lib/cli-config.js', () => ({ @@ -235,7 +234,7 @@ describe('selectFromList', () => { expect(stringGetIdFromUserMock).not.toHaveBeenCalled() expect(booleanConfigValueMock).not.toHaveBeenCalled() - expect(promptMock).not.toHaveBeenCalled() + expect(selectMock).not.toHaveBeenCalled() expect(setConfigKeyMock).not.toHaveBeenCalled() expect(getItemMock).not.toHaveBeenCalled() expect(userMessageMock).not.toHaveBeenCalled() @@ -251,7 +250,7 @@ describe('selectFromList', () => { expect(stringGetIdFromUserMock).toHaveBeenCalledTimes(1) expect(booleanConfigValueMock).not.toHaveBeenCalled() - expect(promptMock).not.toHaveBeenCalled() + expect(selectMock).not.toHaveBeenCalled() expect(setConfigKeyMock).not.toHaveBeenCalled() expect(getItemMock).not.toHaveBeenCalled() expect(userMessageMock).not.toHaveBeenCalled() @@ -275,7 +274,7 @@ describe('selectFromList', () => { expect(stringGetIdFromUserMock).not.toHaveBeenCalled() expect(booleanConfigValueMock).not.toHaveBeenCalled() - expect(promptMock).not.toHaveBeenCalled() + expect(selectMock).not.toHaveBeenCalled() expect(setConfigKeyMock).not.toHaveBeenCalled() expect(userMessageMock).toHaveBeenCalledExactlyOnceWith(item1) expect(resetManagedConfigKeyMock).not.toHaveBeenCalled() @@ -286,7 +285,7 @@ describe('selectFromList', () => { defaultItem: 'default-item-id', }) getItemMock.mockResolvedValueOnce(undefined as unknown as SimpleType) - promptMock.mockResolvedValue({ answer: 'No' }) + selectMock.mockResolvedValue('no') const options = { listItems: listItemsMock, defaultValue } expect(await selectFromList(commandWithDefault, config, options)).toBe('chosen-id') @@ -296,7 +295,7 @@ describe('selectFromList', () => { expect(stringGetIdFromUserMock).toHaveBeenCalledTimes(1) expect(booleanConfigValueMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(setConfigKeyMock).not.toHaveBeenCalled() expect(getItemMock).toHaveBeenCalledExactlyOnceWith('default-item-id') expect(userMessageMock).not.toHaveBeenCalled() @@ -311,7 +310,7 @@ describe('selectFromList', () => { defaultItem: 'default-item-id', }) getItemMock.mockRejectedValueOnce({ response: { status: statusCode } }) - promptMock.mockResolvedValue({ answer: 'No' }) + selectMock.mockResolvedValue('no') const options = { listItems: listItemsMock, defaultValue } expect(await selectFromList(commandWithDefault, config, options)).toBe('chosen-id') @@ -321,7 +320,7 @@ describe('selectFromList', () => { expect(stringGetIdFromUserMock).toHaveBeenCalledTimes(1) expect(booleanConfigValueMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(setConfigKeyMock).not.toHaveBeenCalled() expect(getItemMock).toHaveBeenCalledExactlyOnceWith('default-item-id') expect(userMessageMock).not.toHaveBeenCalled() @@ -335,7 +334,7 @@ describe('selectFromList', () => { defaultItem: 'default-item-id', }) getItemMock.mockRejectedValueOnce(Error('unexpected error')) - promptMock.mockResolvedValue({ answer: 'No' }) + selectMock.mockResolvedValue('no') const options = { listItems: listItemsMock, defaultValue } await expect(selectFromList(commandWithDefault, config, options)) @@ -346,7 +345,7 @@ describe('selectFromList', () => { expect(stringGetIdFromUserMock).not.toHaveBeenCalled() expect(booleanConfigValueMock).not.toHaveBeenCalled() - expect(promptMock).not.toHaveBeenCalled() + expect(selectMock).not.toHaveBeenCalled() expect(setConfigKeyMock).not.toHaveBeenCalled() expect(getItemMock).toHaveBeenCalledExactlyOnceWith('default-item-id') expect(userMessageMock).not.toHaveBeenCalled() @@ -355,7 +354,7 @@ describe('selectFromList', () => { it('does not save default when user asked not to', async () => { booleanConfigValueMock.mockReturnValueOnce(false) - promptMock.mockResolvedValue({ answer: 'No' }) + selectMock.mockResolvedValue('no') expect(await selectFromList(command, config, { listItems: listItemsMock, defaultValue })).toBe('chosen-id') @@ -366,8 +365,8 @@ describe('selectFromList', () => { expect(booleanConfigValueMock) .toHaveBeenCalledExactlyOnceWith('defaultItem::neverAskForSaveAgain') - expect(promptMock).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ type: 'list', name: 'answer' }), + expect(selectMock).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ message: 'Do you want to save this as the default?' }), ) expect(setConfigKeyMock).not.toHaveBeenCalled() }) @@ -384,13 +383,13 @@ describe('selectFromList', () => { expect(booleanConfigValueMock) .toHaveBeenCalledExactlyOnceWith('defaultItem::neverAskForSaveAgain') - expect(promptMock).not.toHaveBeenCalled() + expect(selectMock).not.toHaveBeenCalled() expect(setConfigKeyMock).not.toHaveBeenCalled() }) it('saves selected item as default', async () => { booleanConfigValueMock.mockReturnValueOnce(false) - promptMock.mockResolvedValue({ answer: 'yes' }) + selectMock.mockResolvedValue('yes') expect(await selectFromList(command, config, { listItems: listItemsMock, defaultValue })).toBe('chosen-id') @@ -401,8 +400,8 @@ describe('selectFromList', () => { expect(booleanConfigValueMock) .toHaveBeenCalledExactlyOnceWith('defaultItem::neverAskForSaveAgain') - expect(promptMock).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ type: 'list', name: 'answer' }), + expect(selectMock).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ message: 'Do you want to save this as the default?' }), ) expect(setConfigKeyMock) .toHaveBeenCalledExactlyOnceWith(command.cliConfig, 'defaultItem', 'chosen-id') @@ -412,7 +411,7 @@ describe('selectFromList', () => { it('saves "never ask again" response"', async () => { booleanConfigValueMock.mockReturnValueOnce(false) - promptMock.mockResolvedValue({ answer: 'never' }) + selectMock.mockResolvedValue('never') expect(await selectFromList(command, config, { listItems: listItemsMock, defaultValue })).toBe('chosen-id') @@ -423,8 +422,8 @@ describe('selectFromList', () => { expect(booleanConfigValueMock).toHaveBeenCalledTimes(1) expect(booleanConfigValueMock).toHaveBeenCalledWith('defaultItem::neverAskForSaveAgain') - expect(promptMock).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ type: 'list', name: 'answer' }), + expect(selectMock).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ message: 'Do you want to save this as the default?' }), ) expect(setConfigKeyMock).toHaveBeenCalledExactlyOnceWith( command.cliConfig, diff --git a/src/__tests__/lib/command/util/history.test.ts b/src/__tests__/lib/command/util/history.test.ts index 8a8c209c..b2e53446 100644 --- a/src/__tests__/lib/command/util/history.test.ts +++ b/src/__tests__/lib/command/util/history.test.ts @@ -1,9 +1,10 @@ import { jest } from '@jest/globals' -import type inquirer from 'inquirer' +import type { select } from '@inquirer/prompts' import type { DeviceActivity, HistoryEndpoint, PaginatedList, SmartThingsClient } from '@smartthings/core-sdk' +import type { booleanInput } from '../../../../lib/user-query.js' import type { cancelCommand } from '../../../../lib/util.js' import type { SmartThingsCommand } from '../../../../lib/command/smartthings-command.js' import { @@ -15,11 +16,14 @@ import { } from '../../../test-lib/table-mock.js' -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - }, +const selectMock = jest.fn() +jest.unstable_mockModule('@inquirer/prompts', () => ({ + select: selectMock, +})) + +const booleanInputMock = jest.fn() +jest.unstable_mockModule('../../../../lib/user-query.js', () => ({ + booleanInput: booleanInputMock, })) const cancelCommandMock = jest.fn().mockImplementation(() => { throw Error('command canceled') }) @@ -192,8 +196,8 @@ describe('writeDeviceEventsTable', () => { hasNext.mockReturnValueOnce(true) hasNext.mockReturnValueOnce(true) hasNext.mockReturnValueOnce(false) - promptMock.mockResolvedValueOnce({ more: true }) - promptMock.mockResolvedValueOnce({ more: true }) + booleanInputMock.mockResolvedValueOnce(true) + booleanInputMock.mockResolvedValueOnce(true) await writeDeviceEventsTable(command, dataMock) @@ -204,8 +208,8 @@ describe('writeDeviceEventsTable', () => { it('returns next page until canceled', async () => { hasNext.mockReturnValue(true) - promptMock.mockResolvedValueOnce({ more: true }) - promptMock.mockResolvedValueOnce({ more: false }) + booleanInputMock.mockResolvedValueOnce(true) + booleanInputMock.mockResolvedValueOnce(false) await writeDeviceEventsTable(command, dataMock) @@ -258,7 +262,7 @@ describe('getHistory', () => { expect(apiHistoryDevicesMock).toHaveBeenCalledWith(params) expect(hasNextMock).toHaveBeenCalledTimes(1) expect(nextMock).toHaveBeenCalledTimes(0) - expect(promptMock).toHaveBeenCalledTimes(0) + expect(selectMock).toHaveBeenCalledTimes(0) }) it('makes multiple calls when needed', async () => { @@ -279,17 +283,17 @@ describe('getHistory', () => { expect(apiHistoryDevicesMock).toHaveBeenCalledWith(params) expect(hasNextMock).toHaveBeenCalledTimes(2) expect(nextMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledTimes(0) + expect(selectMock).toHaveBeenCalledTimes(0) }) it('abandons large query when requested to do so', async () => { - promptMock.mockResolvedValueOnce({ answer: 'cancel' }) + selectMock.mockResolvedValueOnce('cancel') await expect( getHistory(client, maxItemsPerRequest * maxRequestsBeforeWarning + 1, maxItemsPerRequest, params), ).rejects.toThrow('command canceled') - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(cancelCommandMock).toHaveBeenCalledExactlyOnceWith('user canceled request') expect(apiHistoryDevicesMock).toHaveBeenCalledTimes(0) expect(hasNextMock).toHaveBeenCalledTimes(0) @@ -297,7 +301,7 @@ describe('getHistory', () => { }) it('limits large query when requested to do so', async () => { - promptMock.mockResolvedValueOnce({ answer: 'reduce' }) + selectMock.mockResolvedValueOnce('reduce') const firstItemSet = items.slice(0, maxItemsPerRequest) const historyResponse = makeHistoryResponse(firstItemSet) apiHistoryDevicesMock.mockResolvedValueOnce(historyResponse) @@ -313,7 +317,7 @@ describe('getHistory', () => { maxItemsPerRequest, params) expect(result).toStrictEqual(items.slice(0, maxRequestsBeforeWarning * maxItemsPerRequest)) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(apiHistoryDevicesMock).toHaveBeenCalledTimes(1) expect(apiHistoryDevicesMock).toHaveBeenCalledWith(params) expect(hasNextMock).toHaveBeenCalledTimes(5) @@ -321,7 +325,7 @@ describe('getHistory', () => { }) it('makes all requests when asked to', async () => { - promptMock.mockResolvedValueOnce({ answer: 'yes' }) + selectMock.mockResolvedValueOnce('yes') const firstItemSet = items.slice(0, maxItemsPerRequest) const historyResponse = makeHistoryResponse(firstItemSet) apiHistoryDevicesMock.mockResolvedValueOnce(historyResponse) @@ -336,7 +340,7 @@ describe('getHistory', () => { const result = await getHistory(client, items.length, maxItemsPerRequest, params) expect(result).toStrictEqual(items) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(apiHistoryDevicesMock).toHaveBeenCalledTimes(1) expect(apiHistoryDevicesMock).toHaveBeenCalledWith(params) expect(hasNextMock).toHaveBeenCalledTimes(6) @@ -344,7 +348,7 @@ describe('getHistory', () => { }) it('ignores some result from last request to return no more than requested limit', async () => { - promptMock.mockResolvedValueOnce({ answer: 'yes' }) + selectMock.mockResolvedValueOnce('yes') const firstItemSet = items.slice(0, maxItemsPerRequest) const historyResponse = makeHistoryResponse(firstItemSet) apiHistoryDevicesMock.mockResolvedValueOnce(historyResponse) @@ -359,7 +363,7 @@ describe('getHistory', () => { const result = await getHistory(client, items.length - 5, maxItemsPerRequest, params) expect(result).toStrictEqual(items.slice(0, items.length - 5)) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(apiHistoryDevicesMock).toHaveBeenCalledTimes(1) expect(apiHistoryDevicesMock).toHaveBeenCalledWith(params) expect(hasNextMock).toHaveBeenCalledTimes(6) diff --git a/src/lib/command/command-util.ts b/src/lib/command/command-util.ts index cc60ff24..54bdafbc 100644 --- a/src/lib/command/command-util.ts +++ b/src/lib/command/command-util.ts @@ -1,5 +1,4 @@ -import inquirer from 'inquirer' - +import { stringInput } from '../user-query.js' import { ListDataFunction, Naming, Sorting } from './io-defs.js' import { sort } from './output.js' @@ -81,16 +80,12 @@ export function convertToId(itemIdOrIndex: string, primaryKeyName: Extract(fieldInfo: Sorting, list: L[], prompt?: string): Promise { const primaryKeyName = fieldInfo.primaryKeyName - const itemIdOrIndex: string = (await inquirer.prompt({ - type: 'input', - name: 'itemIdOrIndex', - message: prompt ?? 'Enter id or index', - validate: input => { - return convertToId(input, primaryKeyName, list) + const itemIdOrIndex: string = await stringInput(prompt ?? 'Enter id or index', { + validate: input => + convertToId(input, primaryKeyName, list) ? true - : `Invalid id or index "${input}". Please enter an index or valid id.` - }, - })).itemIdOrIndex + : `Invalid id or index "${input}". Please enter an index or valid id.`, + }) const inputId = convertToId(itemIdOrIndex, primaryKeyName, list) if (inputId === false) { throw Error(`unable to convert ${itemIdOrIndex} to id`) diff --git a/src/lib/command/select.ts b/src/lib/command/select.ts index b7bea5b3..99ec8b27 100644 --- a/src/lib/command/select.ts +++ b/src/lib/command/select.ts @@ -1,4 +1,4 @@ -import inquirer from 'inquirer' +import { select } from '@inquirer/prompts' import { type IdRetrievalFunction, @@ -171,16 +171,14 @@ export async function selectFromList( const neverAgainKey = `${options.defaultValue?.configKey ?? ''}::neverAskForSaveAgain` if (options.defaultValue && !command.cliConfig.booleanConfigValue(neverAgainKey)) { - const answer = (await inquirer.prompt({ - type: 'list', - name: 'answer', + const answer = await select({ message: 'Do you want to save this as the default?', choices: [ { name: 'Yes', value: 'yes' }, { name: 'No', value: 'no' }, { name: 'No, and do not ask again', value: 'never' }, ], - })).answer as string + }) const resetInfo = 'You can reset these settings using the config:reset command.' if (answer === 'yes') { diff --git a/src/lib/command/util/history.ts b/src/lib/command/util/history.ts index 261b426a..68f2383e 100644 --- a/src/lib/command/util/history.ts +++ b/src/lib/command/util/history.ts @@ -1,4 +1,4 @@ -import inquirer from 'inquirer' +import { select } from '@inquirer/prompts' import { type DeviceActivity, @@ -8,6 +8,7 @@ import { } from '@smartthings/core-sdk' import { type Table } from '../../table-generator.js' +import { booleanInput } from '../../user-query.js' import { cancelCommand } from '../../util.js' import { type SmartThingsCommand } from '../smartthings-command.js' @@ -65,12 +66,7 @@ export const writeDeviceEventsTable = async ( process.stdout.write(table.toString()) while (data.hasNext()) { - const more = (await inquirer.prompt({ - type: 'confirm', - name: 'more', - message: 'Fetch more history records?', - default: true, - })).more as boolean + const more = await booleanInput('Fetch more history records?') if (!more) { break @@ -100,9 +96,7 @@ export const getHistory = async ( const requestsToMake = Math.ceil(limit / maxItemsPerRequest) if (requestsToMake > maxRequestsBeforeWarning) { // prompt user if it's okay to continue - const answer = (await inquirer.prompt({ - type: 'list', - name: 'answer', + const answer = await select({ message: `Querying ${limit} history items will result in ${requestsToMake} requests.\n` + 'Are you sure you want to continue?', choices: [ @@ -113,7 +107,7 @@ export const getHistory = async ( value: 'reduce', }, ], - })).answer as string + }) if (answer === 'reduce') { limit = maxRequestsBeforeWarning * maxItemsPerRequest } else if (answer === 'cancel') {