From fbce451ad623fbbb773e470072a2cbf531ce0736 Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Thu, 12 Jun 2025 10:18:15 -0500 Subject: [PATCH] refactor: update item-input module to use new inquirer --- .../util/apps-input-primitives.test.ts | 4 +- src/__tests__/lib/item-input/array.test.ts | 241 +++++++++--------- .../lib/item-input/command-helpers.test.ts | 103 ++++---- src/__tests__/lib/item-input/misc.test.ts | 32 +-- src/__tests__/lib/item-input/object.test.ts | 110 ++++---- src/__tests__/lib/item-input/select.test.ts | 51 ++-- src/lib/command/util/apps-input-primitives.ts | 4 +- src/lib/item-input/array.ts | 61 +++-- src/lib/item-input/command-helpers.ts | 23 +- src/lib/item-input/defs.ts | 38 ++- src/lib/item-input/misc.ts | 17 +- src/lib/item-input/object.ts | 28 +- src/lib/item-input/select.ts | 14 +- 13 files changed, 364 insertions(+), 362 deletions(-) diff --git a/src/__tests__/lib/command/util/apps-input-primitives.test.ts b/src/__tests__/lib/command/util/apps-input-primitives.test.ts index dfcb4565..23f74d12 100644 --- a/src/__tests__/lib/command/util/apps-input-primitives.test.ts +++ b/src/__tests__/lib/command/util/apps-input-primitives.test.ts @@ -45,6 +45,6 @@ const validate = checkboxDefMock.mock.calls[0][2]?.validate test('oauthAppScopeDef requires at least one scope', () => { expect(validate?.([])).toBe('At least one scope is required.') - expect(validate?.(['scope'])).toBe(true) - expect(validate?.(['scope1', 'scope2'])).toBe(true) + expect(validate?.([{ name: 'scope', value: 'scope' }])).toBe(true) + expect(validate?.([{ name: 'scope1', value: 'scope1' }, { name: 'scope2', value: 'scope2' }])).toBe(true) }) diff --git a/src/__tests__/lib/item-input/array.test.ts b/src/__tests__/lib/item-input/array.test.ts index dcf694b5..f79d0732 100644 --- a/src/__tests__/lib/item-input/array.test.ts +++ b/src/__tests__/lib/item-input/array.test.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' -import inquirer from 'inquirer' +import { type checkbox, type select, Separator } from '@inquirer/prompts' import { addAction, @@ -19,13 +19,14 @@ import { ArrayDefOptions, CheckboxDefOptions } from '../../../lib/item-input/arr import { buildInputDefMock } from '../../test-lib/input-type-mock.js' -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - Separator: inquirer.Separator, - }, +const checkboxMock = jest.fn() +const selectMock = jest.fn() +jest.unstable_mockModule('@inquirer/prompts', () => ({ + checkbox: checkboxMock, + select: selectMock, + Separator, })) + const clipToMaximumMock = jest.fn() clipToMaximumMock.mockReturnValue('clipped') const stringFromUnknownMock = jest.fn() @@ -59,7 +60,7 @@ describe('arrayDef', () => { describe('buildFromUserInput', () => { it('does not allow uneditable items', async () => { - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('entered value') itemSummarizeForEditMock.mockReturnValueOnce(uneditable) @@ -71,14 +72,14 @@ describe('arrayDef', () => { }) it('requires at least one item by default', async () => { - promptMock.mockResolvedValueOnce({ action: addAction }) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(addAction) + selectMock.mockResolvedValueOnce(finishAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item1') expect(await def.buildFromUserInput()).toStrictEqual(['item1']) - expect(promptMock).toHaveBeenCalledTimes(2) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ message: 'Add or edit Array Def.', default: addAction, choices: [ @@ -86,7 +87,7 @@ describe('arrayDef', () => { cancelOption, ], })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Add Item Def.', value: addAction }, cancelOption, @@ -99,16 +100,16 @@ describe('arrayDef', () => { }) it('does not allow duplicates by default', async () => { - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item1') - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item1') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await def.buildFromUserInput()).toStrictEqual(['item1']) - expect(promptMock).toHaveBeenCalledTimes(3) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ message: 'Add or edit Array Def.', default: addAction, choices: [ @@ -116,7 +117,7 @@ describe('arrayDef', () => { cancelOption, ], })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Add Item Def.', value: addAction }, cancelOption, @@ -124,7 +125,7 @@ describe('arrayDef', () => { ]), default: finishAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Add Item Def.', value: addAction }, cancelOption, @@ -141,16 +142,16 @@ describe('arrayDef', () => { it('allows duplicates if specified', async () => { const def = arrayDef('Array Def', itemDefMock, { allowDuplicates: true }) - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item1') - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item1') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await def.buildFromUserInput()).toStrictEqual(['item1', 'item1']) - expect(promptMock).toHaveBeenCalledTimes(3) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ message: 'Add or edit Array Def.', default: addAction, choices: [ @@ -158,7 +159,7 @@ describe('arrayDef', () => { cancelOption, ], })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Add Item Def.', value: addAction }, cancelOption, @@ -166,7 +167,7 @@ describe('arrayDef', () => { ]), default: finishAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Add Item Def.', value: addAction }, cancelOption, @@ -182,26 +183,26 @@ describe('arrayDef', () => { it('allows many items by default', async () => { for (let index = 1; index <= 1000; index++) { - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce(`item${index}`) } - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) const result = await def.buildFromUserInput() as string[] expect(result.length).toBe(1000) - expect(promptMock).toHaveBeenCalledTimes(1001) + expect(selectMock).toHaveBeenCalledTimes(1001) expect(itemBuildFromUserInputMock).toHaveBeenCalledTimes(1000) }) it('allows empty array when asked to do so', async () => { const def = arrayDef('Array Def', itemDefMock, { minItems: 0 }) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await def.buildFromUserInput()).toStrictEqual([]) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Add or edit Array Def.', default: finishAction, choices: [ @@ -217,36 +218,36 @@ describe('arrayDef', () => { it('requires specified minium', async () => { const def = arrayDef('Array Def', itemDefMock, { minItems: 2 }) - promptMock.mockResolvedValueOnce({ action: addAction }) - promptMock.mockResolvedValueOnce({ action: addAction }) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(addAction) + selectMock.mockResolvedValueOnce(addAction) + selectMock.mockResolvedValueOnce(finishAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item1') itemBuildFromUserInputMock.mockResolvedValueOnce('item2') expect(await def.buildFromUserInput()).toStrictEqual(['item1', 'item2']) - expect(promptMock).toHaveBeenCalledTimes(3) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ default: addAction, choices: [ { name: 'Add Item Def.', value: addAction }, cancelOption, ], })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: [ { name: 'Edit summarized item1.', value: 0 }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Add Item Def.', value: addAction }, cancelOption, ], default: addAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: [ { name: 'Edit summarized item1.', value: 0 }, { name: 'Edit summarized item2.', value: 1 }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Add Item Def.', value: addAction }, { name: 'Finish editing Array Def.', value: finishAction }, cancelOption, @@ -262,51 +263,51 @@ describe('arrayDef', () => { it('allows no more than specified maximum', async () => { const def = arrayDef('Array Def', itemDefMock, { maxItems: 3 }) - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item1') - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item2') - promptMock.mockResolvedValueOnce({ action: addAction }) + selectMock.mockResolvedValueOnce(addAction) itemBuildFromUserInputMock.mockResolvedValueOnce('item3') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await def.buildFromUserInput()).toStrictEqual(['item1', 'item2', 'item3']) - expect(promptMock).toHaveBeenCalledTimes(4) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(4) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: [ { name: 'Add Item Def.', value: addAction }, cancelOption, ], default: addAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: [ { name: 'Edit summarized item1.', value: 0 }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Add Item Def.', value: addAction }, { name: 'Finish editing Array Def.', value: finishAction }, cancelOption, ], default: finishAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: [ { name: 'Edit summarized item1.', value: 0 }, { name: 'Edit summarized item2.', value: 1 }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Add Item Def.', value: addAction }, { name: 'Finish editing Array Def.', value: finishAction }, cancelOption, ], default: finishAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: [ { name: 'Edit summarized item1.', value: 0 }, { name: 'Edit summarized item2.', value: 1 }, { name: 'Edit summarized item3.', value: 2 }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Finish editing Array Def.', value: finishAction }, cancelOption, ], @@ -320,35 +321,34 @@ describe('arrayDef', () => { }) it('returns cancelAction when canceled', async () => { - promptMock.mockResolvedValueOnce({ action: cancelAction }) + selectMock.mockResolvedValueOnce(cancelAction) expect(await def.buildFromUserInput()).toBe(cancelAction) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(itemBuildFromUserInputMock).not.toHaveBeenCalled() }) it('throws error on invalid action', async () => { // This is a "should-never-happen" error that we can artificially create with mocking. - promptMock.mockResolvedValueOnce({ action: 'bad action' }) + selectMock.mockResolvedValueOnce('bad action') - await expect(def.buildFromUserInput()).rejects - .toThrow('unexpected state in arrayDef; action = "bad action"') + await expect(def.buildFromUserInput()).rejects.toThrow('unexpected state in arrayDef') - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(itemBuildFromUserInputMock).not.toHaveBeenCalled() }) it('includes help option when helpText is supplied', async () => { - promptMock.mockResolvedValueOnce({ action: helpAction }) - promptMock.mockResolvedValueOnce({ action: cancelAction }) + selectMock.mockResolvedValueOnce(helpAction) + selectMock.mockResolvedValueOnce(cancelAction) const def = arrayDef('Array Def', itemDefMock, { helpText: 'help text' }) expect(await def.buildFromUserInput()).toBe(cancelAction) - expect(promptMock).toHaveBeenCalledTimes(2) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ message: 'Add or edit Array Def.', default: addAction, choices: [ @@ -367,14 +367,14 @@ describe('arrayDef', () => { it('logs undefined for help text in unexpected circumstance', async () => { // Here is where we mock something that can't happen since `helpAction` isn't included // when no help text is supplied. - promptMock.mockResolvedValueOnce({ action: helpAction }) - promptMock.mockResolvedValueOnce({ action: cancelAction }) + selectMock.mockResolvedValueOnce(helpAction) + selectMock.mockResolvedValueOnce(cancelAction) const def = arrayDef('Array Def', itemDefMock) expect(await def.buildFromUserInput()).toBe(cancelAction) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(consoleLogSpy).toHaveBeenCalledWith('\nundefined\n') }) @@ -412,15 +412,15 @@ describe('arrayDef', () => { // Shares code from buildFromUserInput so much of this is tested there. it('doesn\'t allow removal of last item by default', async () => { - promptMock.mockResolvedValueOnce({ action: 0 }) // from editList - promptMock.mockResolvedValueOnce({ action: cancelAction }) // from editItem - promptMock.mockResolvedValueOnce({ action: finishAction }) // from editList + selectMock.mockResolvedValueOnce(0) // from editList + selectMock.mockResolvedValueOnce(cancelAction) // from editItem + selectMock.mockResolvedValueOnce(finishAction) // from editList expect(await def.updateFromUserInput(['item1'])).toStrictEqual(['item1']) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) // The prompt in editItem should have no delete option. - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: [ { name: 'Edit summarized item1.', value: editAction }, cancelOption, @@ -431,42 +431,42 @@ describe('arrayDef', () => { }) it('allows editing of an item', async () => { - promptMock.mockResolvedValueOnce({ action: 1 }) // from editList - promptMock.mockResolvedValueOnce({ action: editAction }) // from editItem + selectMock.mockResolvedValueOnce(1) // from editList + selectMock.mockResolvedValueOnce(editAction) // from editItem itemUpdateFromUserInputMock.mockResolvedValueOnce('updated item2') - promptMock.mockResolvedValueOnce({ action: finishAction }) // from editList + selectMock.mockResolvedValueOnce(finishAction) // from editList expect(await def.updateFromUserInput(['item1', 'item2', 'item3'])) .toStrictEqual(['item1', 'updated item2', 'item3']) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) expect(itemUpdateFromUserInputMock).toHaveBeenCalledTimes(1) }) it('does not allow duplicates by default', async () => { - promptMock.mockResolvedValueOnce({ action: 1 }) // from editList - promptMock.mockResolvedValueOnce({ action: editAction }) // from editItem + selectMock.mockResolvedValueOnce(1) // from editList + selectMock.mockResolvedValueOnce(editAction) // from editItem itemUpdateFromUserInputMock.mockResolvedValueOnce('item3') // `item3` is already used! - promptMock.mockResolvedValueOnce({ action: finishAction }) // from editList + selectMock.mockResolvedValueOnce(finishAction) // from editList expect(await def.updateFromUserInput(['item1', 'item2', 'item3'])) .toStrictEqual(['item1', 'item2', 'item3']) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) expect(consoleLogSpy).toHaveBeenCalledWith('Duplicate values are not allowed.') expect(itemUpdateFromUserInputMock).toHaveBeenCalledExactlyOnceWith('item2', []) }) it('always allow reentering current value even when duplicates are not allowed', async () => { - promptMock.mockResolvedValueOnce({ action: 1 }) // from editList - promptMock.mockResolvedValueOnce({ action: editAction }) // from editItem + selectMock.mockResolvedValueOnce(1) // from editList + selectMock.mockResolvedValueOnce(editAction) // from editItem itemUpdateFromUserInputMock.mockResolvedValueOnce('item2') // not changing the value - promptMock.mockResolvedValueOnce({ action: finishAction }) // from editList + selectMock.mockResolvedValueOnce(finishAction) // from editList expect(await def.updateFromUserInput(['item1', 'item2', 'item3'])) .toStrictEqual(['item1', 'item2', 'item3']) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) expect(consoleLogSpy).not.toHaveBeenCalled() expect(itemUpdateFromUserInputMock).toHaveBeenCalledExactlyOnceWith('item2', []) }) @@ -474,37 +474,37 @@ describe('arrayDef', () => { it('allows duplicates if specified', async () => { const def = arrayDef('Array Def', itemDefMock, { allowDuplicates: true }) - promptMock.mockResolvedValueOnce({ action: 1 }) // from editList - promptMock.mockResolvedValueOnce({ action: editAction }) // from editItem + selectMock.mockResolvedValueOnce(1) // from editList + selectMock.mockResolvedValueOnce(editAction) // from editItem itemUpdateFromUserInputMock.mockResolvedValueOnce('item3') // `item3` is already used! - promptMock.mockResolvedValueOnce({ action: finishAction }) // from editList + selectMock.mockResolvedValueOnce(finishAction) // from editList expect(await def.updateFromUserInput(['item1', 'item2', 'item3'])) .toStrictEqual(['item1', 'item3', 'item3']) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) expect(consoleLogSpy).not.toHaveBeenCalled() expect(itemUpdateFromUserInputMock).toHaveBeenCalledExactlyOnceWith('item2', []) }) it('supports item removal', async () => { - promptMock.mockResolvedValueOnce({ action: 1 }) // from editList - promptMock.mockResolvedValueOnce({ action: deleteAction }) // from editItem - promptMock.mockResolvedValueOnce({ action: finishAction }) // from editList + selectMock.mockResolvedValueOnce(1) // from editList + selectMock.mockResolvedValueOnce(deleteAction) // from editItem + selectMock.mockResolvedValueOnce(finishAction) // from editList expect(await def.updateFromUserInput(['item1', 'item2', 'item3'])) .toStrictEqual(['item1', 'item3']) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) expect(itemUpdateFromUserInputMock).not.toHaveBeenCalled() }) it('returns cancelAction when canceled', async () => { - promptMock.mockResolvedValueOnce({ action: cancelAction }) + selectMock.mockResolvedValueOnce(cancelAction) expect(await def.updateFromUserInput(['item1', 'item2'])).toBe(cancelAction) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) expect(itemUpdateFromUserInputMock).not.toHaveBeenCalled() }) }) @@ -527,29 +527,28 @@ describe('checkboxDef', () => { }) describe('buildFromUserInput', () => { - it('uses simple string values as choices', async () => { - promptMock.mockResolvedValueOnce({ values: simpleResults }) + it('accepts simple string values as choices', async () => { + checkboxMock.mockResolvedValueOnce(simpleResults) expect(await simpleDef.buildFromUserInput()).toBe(simpleResults) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ - type: 'checkbox', - name: 'values', + expect(checkboxMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Select Checkbox Def.', - choices: ['Item 1', 'Item 2', 'Item 3'], + choices: [ + { name: 'Item 1', value: 'Item 1' }, + { name: 'Item 2', value: 'Item 2' }, + { name: 'Item 3', value: 'Item 3' }, + ], default: finishAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.not.objectContaining({ - validate: undefined, - })) }) - it('uses complex values as values in choices', async () => { - promptMock.mockResolvedValueOnce({ values: complexResults }) + it('accepts complex values as values in choices', async () => { + checkboxMock.mockResolvedValueOnce(complexResults) expect(await complexDef.buildFromUserInput()).toBe(complexResults) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(checkboxMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ choices: complexChoices, default: finishAction, })) @@ -562,13 +561,13 @@ describe('checkboxDef', () => { { default: ['Item 2', 'Item 3'] }, ) - promptMock.mockResolvedValueOnce({ values: simpleResults }) + checkboxMock.mockResolvedValueOnce(simpleResults) expect(await def.buildFromUserInput()).toBe(simpleResults) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(checkboxMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ choices: [ - 'Item 1', + { name: 'Item 1', value: 'Item 1' }, { name: 'Item 2', value: 'Item 2', checked: true }, { name: 'Item 3', value: 'Item 3', checked: true }, ], @@ -583,11 +582,11 @@ describe('checkboxDef', () => { { default: [complexValues[0]] }, ) - promptMock.mockResolvedValueOnce({ values: complexResults }) + checkboxMock.mockResolvedValueOnce(complexResults) expect(await def.buildFromUserInput()).toBe(complexResults) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(checkboxMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ choices: [ { ...complexChoices[0], checked: true }, complexChoices[1], @@ -600,11 +599,11 @@ describe('checkboxDef', () => { it('uses supplied validate method', async () => { const validate = jest.fn>['validate']>() const def = checkboxDef('Checkbox Def', ['Item 1', 'Item 2', 'Item 3'], { validate }) - promptMock.mockResolvedValueOnce({ values: simpleResults }) + checkboxMock.mockResolvedValueOnce(simpleResults) expect(await def.buildFromUserInput()).toBe(simpleResults) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(checkboxMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ validate, })) }) @@ -615,20 +614,18 @@ describe('checkboxDef', () => { ['Item 1', 'Item 2'], { helpText: 'help text' }, ) - promptMock.mockResolvedValueOnce({ values: ['Item 1'] }) + checkboxMock.mockResolvedValueOnce(['Item 1']) expect(await def.buildFromUserInput()).toStrictEqual(['Item 1']) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ - type: 'checkbox', - name: 'values', + expect(checkboxMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Select Checkbox Def.', - choices: ['Item 1', 'Item 2'], + choices: [ + { name: 'Item 1', value: 'Item 1' }, + { name: 'Item 2', value: 'Item 2' }, + ], default: finishAction, })) - expect(promptMock).toHaveBeenCalledWith(expect.not.objectContaining({ - validate: undefined, - })) expect(consoleLogSpy).toHaveBeenCalledWith('\nhelp text\n') }) @@ -673,11 +670,11 @@ describe('checkboxDef', () => { it('preselects previous values', async () => { const def = checkboxDef('Complex Checkbox Def', complexChoices) - promptMock.mockResolvedValueOnce({ values: complexResults }) + checkboxMock.mockResolvedValueOnce(complexResults) expect(await def.updateFromUserInput([complexValues[1]])).toBe(complexResults) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(checkboxMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ choices: [ complexChoices[0], { ...complexChoices[1], checked: true }, diff --git a/src/__tests__/lib/item-input/command-helpers.test.ts b/src/__tests__/lib/item-input/command-helpers.test.ts index e557c02f..0660bbd4 100644 --- a/src/__tests__/lib/item-input/command-helpers.test.ts +++ b/src/__tests__/lib/item-input/command-helpers.test.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' -import inquirer from 'inquirer' +import type { select } from '@inquirer/prompts' import type { Profile } from '../../../lib/cli-config.js' import type { red } from '../../../lib/colors.js' @@ -24,12 +24,9 @@ import { buildInputDefMock } from '../../test-lib/input-type-mock.js' import type { SimpleType } from '../../test-lib/simple-type.js' -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - Separator: inquirer.Separator, - }, +const selectMock = jest.fn() +jest.unstable_mockModule('@inquirer/prompts', () => ({ + select: selectMock, })) const redMock = jest.fn() @@ -93,12 +90,12 @@ const badInputValue = { str: 'a bad string', num: -1_000_000 } describe('updateFromUserInput', () => { it('returns input when no further updates requested', async () => { - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toStrictEqual(simpleValue) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Choose an action.', default: finishAction, choices: [ @@ -116,13 +113,13 @@ describe('updateFromUserInput', () => { }) it('uses "output" text when in dry-run mode', async () => { - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: true })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Finish and output Item-to-Input.', value: finishAction }, ]), @@ -130,7 +127,7 @@ describe('updateFromUserInput', () => { }) it('uses specified finishVerb', async () => { - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput( commandMock, @@ -139,7 +136,7 @@ describe('updateFromUserInput', () => { { dryRun: false, finishVerb: 'create' }, )).toBe(simpleValue) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Finish and create Item-to-Input.', value: finishAction }, ]), @@ -147,14 +144,14 @@ describe('updateFromUserInput', () => { }) it('allows editing from main menu', async () => { - promptMock.mockResolvedValueOnce({ action: editAction }) + selectMock.mockResolvedValueOnce(editAction) updateFromUserInputMock.mockResolvedValueOnce(updatedValue) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(updatedValue) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(updateFromUserInputMock).toHaveBeenCalledExactlyOnceWith(simpleValue) }) @@ -169,7 +166,7 @@ describe('updateFromUserInput', () => { ...inputDefMock, validateFinal: validateFinalMock, } - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput( commandMock, @@ -184,10 +181,10 @@ describe('updateFromUserInput', () => { expect(redMock).toHaveBeenCalledExactlyOnceWith('error text') expect(consoleLogSpy).toHaveBeenCalledWith('red error text') expect(updateFromUserInputMock).toHaveBeenCalledExactlyOnceWith(badInputValue) - expect(promptMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledTimes(1) }) - it('allows cancelation with validation error', async () => { + it('allows cancellation with validation error', async () => { const validateFinalMock = jest.fn>['validateFinal']>() .mockReturnValue('error text') redMock.mockReturnValueOnce('red error text') @@ -211,30 +208,30 @@ describe('updateFromUserInput', () => { expect(updateFromUserInputMock).toHaveBeenCalledExactlyOnceWith(badInputValue) expect(cancelCommandMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledTimes(0) + expect(selectMock).toHaveBeenCalledTimes(0) }) - it('accepts cancelation of editing from main menu', async () => { - promptMock.mockResolvedValueOnce({ action: editAction }) + it('accepts cancellation of editing from main menu', async () => { + selectMock.mockResolvedValueOnce(editAction) updateFromUserInputMock.mockResolvedValueOnce(cancelAction) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(updateFromUserInputMock).toHaveBeenCalledExactlyOnceWith(simpleValue) }) it('allows previewing JSON', async () => { - promptMock.mockResolvedValueOnce({ action: previewJSONAction }) + selectMock.mockResolvedValueOnce(previewJSONAction) booleanInputMock.mockResolvedValueOnce(false) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(jsonFormatterMock).toHaveBeenCalledExactlyOnceWith(4) expect(jsonOutputFormatterMock).toHaveBeenCalledTimes(1) expect(jsonOutputFormatterMock).toHaveBeenCalledWith(simpleValue) @@ -245,44 +242,44 @@ describe('updateFromUserInput', () => { }) it('allows editing after preview', async () => { - promptMock.mockResolvedValueOnce({ action: previewJSONAction }) + selectMock.mockResolvedValueOnce(previewJSONAction) booleanInputMock.mockResolvedValueOnce(true) updateFromUserInputMock.mockResolvedValueOnce(updatedValue) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(updatedValue) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(jsonFormatterMock).toHaveBeenCalledTimes(1) expect(jsonOutputFormatterMock).toHaveBeenCalledTimes(1) expect(updateFromUserInputMock).toHaveBeenCalledExactlyOnceWith(simpleValue) }) - it('sticks to original upon cancelation after editing after preview', async () => { - promptMock.mockResolvedValueOnce({ action: previewJSONAction }) + it('sticks to original upon cancellation after editing after preview', async () => { + selectMock.mockResolvedValueOnce(previewJSONAction) booleanInputMock.mockResolvedValueOnce(true) updateFromUserInputMock.mockResolvedValueOnce(cancelAction) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(jsonFormatterMock).toHaveBeenCalledTimes(1) expect(jsonOutputFormatterMock).toHaveBeenCalledTimes(1) expect(updateFromUserInputMock).toHaveBeenCalledExactlyOnceWith(simpleValue) }) it('allows previewing YAML', async () => { - promptMock.mockResolvedValueOnce({ action: previewYAMLAction }) + selectMock.mockResolvedValueOnce(previewYAMLAction) booleanInputMock.mockResolvedValueOnce(false) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(yamlFormatterMock).toHaveBeenCalledExactlyOnceWith(2) expect(yamlOutputFormatterMock).toHaveBeenCalledExactlyOnceWith(simpleValue) expect(booleanInputMock).toHaveBeenCalledWith( @@ -293,48 +290,48 @@ describe('updateFromUserInput', () => { it('accepts indent level from config', async () => { const commandMock = buildCommandMock({}, { indent: 13 }) - promptMock.mockResolvedValueOnce({ action: previewYAMLAction }) + selectMock.mockResolvedValueOnce(previewYAMLAction) booleanInputMock.mockResolvedValueOnce(false) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(yamlFormatterMock).toHaveBeenCalledExactlyOnceWith(13) expect(yamlOutputFormatterMock).toHaveBeenCalledTimes(1) }) it('accepts indent level from command line', async () => { const commandMock = buildCommandMock({ indent: 14 }) - promptMock.mockResolvedValueOnce({ action: previewJSONAction }) - promptMock.mockResolvedValueOnce({ editAgain: false }) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(previewJSONAction) + selectMock.mockResolvedValueOnce({ editAgain: false }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) expect(jsonFormatterMock).toHaveBeenCalledExactlyOnceWith(14) expect(jsonOutputFormatterMock).toHaveBeenCalledTimes(1) }) it('command line indent overrides value from config', async () => { const commandMock = buildCommandMock({ indent: 15 }, { indent: 14 }) - promptMock.mockResolvedValueOnce({ action: previewYAMLAction }) - promptMock.mockResolvedValueOnce({ editAgain: false }) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(previewYAMLAction) + selectMock.mockResolvedValueOnce({ editAgain: false }) + selectMock.mockResolvedValueOnce(finishAction) expect(await updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .toBe(simpleValue) - expect(promptMock).toHaveBeenCalledTimes(3) + expect(selectMock).toHaveBeenCalledTimes(3) expect(yamlFormatterMock).toHaveBeenCalledExactlyOnceWith(15) expect(yamlOutputFormatterMock).toHaveBeenCalledTimes(1) }) it('calls cancel when user cancels', async () => { - promptMock.mockResolvedValueOnce({ action: cancelAction }) + selectMock.mockResolvedValueOnce(cancelAction) await expect(updateFromUserInput(commandMock, inputDefMock, simpleValue, { dryRun: false })) .rejects.toThrow('canceled') @@ -346,15 +343,15 @@ describe('updateFromUserInput', () => { describe('createFromUserInput', () => { it('calls buildFromUserInput and updateFromUserInput', async () => { buildFromUserInputMock.mockResolvedValueOnce(simpleValue) - promptMock.mockResolvedValueOnce({ action: editAction }) + selectMock.mockResolvedValueOnce(editAction) updateFromUserInputMock.mockResolvedValueOnce(updatedValue) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) expect(await createFromUserInput(commandMock, inputDefMock, { dryRun: false })) .toStrictEqual(updatedValue) expect(buildFromUserInputMock).toHaveBeenCalledExactlyOnceWith() - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(updateFromUserInputMock).toHaveBeenCalledExactlyOnceWith(simpleValue) }) diff --git a/src/__tests__/lib/item-input/misc.test.ts b/src/__tests__/lib/item-input/misc.test.ts index 1ad12727..e95d31bd 100644 --- a/src/__tests__/lib/item-input/misc.test.ts +++ b/src/__tests__/lib/item-input/misc.test.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' -import inquirer from 'inquirer' +import { type select, Separator } from '@inquirer/prompts' import { cancelOption, @@ -22,12 +22,10 @@ import type { ListSelectionDefOptions, OptionalDefPredicateFn } from '../../../l import { buildInputDefMock } from '../../test-lib/input-type-mock.js' -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - Separator: inquirer.Separator, - }, +const selectMock = jest.fn() +jest.unstable_mockModule('@inquirer/prompts', () => ({ + select: selectMock, + Separator, })) const booleanInputMock = jest.fn() const integerInputMock = jest.fn() @@ -483,7 +481,7 @@ describe('listSelectionDef', () => { it('builds choices from given options and adds cancel option', async () => { const summarizeForEditMock = jest.fn>['summarizeForEdit']>() .mockImplementation((input: string): string => `${input} summary`) - promptMock.mockResolvedValueOnce({ chosen: 'updated value' }) + selectMock.mockResolvedValueOnce('updated value') const def = listSelectionDef('Thing One', ['un', 'dos', 'tres'], { summarizeForEdit: summarizeForEditMock }) expect(await def.updateFromUserInput('original value')).toBe('updated value') @@ -492,10 +490,8 @@ describe('listSelectionDef', () => { expect(summarizeForEditMock).toHaveBeenCalledWith('un') expect(summarizeForEditMock).toHaveBeenCalledWith('dos') expect(summarizeForEditMock).toHaveBeenCalledWith('tres') - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith({ - type: 'list', - name: 'chosen', + expect(selectMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledWith({ message: 'Select Thing One:', choices: [ { name: 'un summary', value: 'un' }, @@ -512,25 +508,25 @@ describe('listSelectionDef', () => { describe('buildFromUserInput', () => { it('proxies to update', async () => { - promptMock.mockResolvedValueOnce({ chosen: 'chosen value' }) + selectMock.mockResolvedValueOnce('chosen value') const def = listSelectionDef('Thing One', ['un', 'dos', 'tres']) expect(await def.buildFromUserInput()).toBe('chosen value') - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ default: undefined, })) }) it('passes on default value to update', async () => { - promptMock.mockResolvedValueOnce({ chosen: 'chosen value' }) + selectMock.mockResolvedValueOnce('chosen value') const def = listSelectionDef('Thing One', ['un', 'dos', 'tres'], { default: 'default value' }) expect(await def.buildFromUserInput()).toBe('chosen value') - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ default: 'default value', })) }) diff --git a/src/__tests__/lib/item-input/object.test.ts b/src/__tests__/lib/item-input/object.test.ts index 59a1d8b4..7ccb46db 100644 --- a/src/__tests__/lib/item-input/object.test.ts +++ b/src/__tests__/lib/item-input/object.test.ts @@ -1,18 +1,22 @@ import { jest } from '@jest/globals' -import inquirer from 'inquirer' - -import { cancelAction, finishAction, helpAction, InputDefinition, inquirerPageSize } from '../../../lib/item-input/defs.js' -import { ObjectDefOptions, ObjectItemTypeData } from '../../../lib/item-input/object.js' +import { type select, Separator } from '@inquirer/prompts' + +import { + cancelAction, + finishAction, + helpAction, + type InputDefinition, + inquirerPageSize, +} from '../../../lib/item-input/defs.js' +import type { ObjectDefOptions, ObjectItemTypeData } from '../../../lib/item-input/object.js' import { buildInputDefMock } from '../../test-lib/input-type-mock.js' -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - Separator: inquirer.Separator, - }, +const selectMock = jest.fn() +jest.unstable_mockModule('@inquirer/prompts', () => ({ + select: selectMock, + Separator, })) @@ -202,31 +206,29 @@ describe('objectDef', () => { input1SummarizeForEditMock.mockReturnValue('Summarized Input 1') input2SummarizeForEditMock.mockReturnValue('Summarized Input 2') input3SummarizeForEditMock.mockReturnValue('Summarized Input 3') - promptMock.mockResolvedValueOnce({ action: 'input2' }) + selectMock.mockResolvedValueOnce('input2') input2UpdateFromUserInputMock.mockResolvedValueOnce('Updated Input 2') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) const original = { input1: 'Item Value 1', input2: 'Item Value 2', 'input3': 'Item Value 3' } expect(await threeInputDef.updateFromUserInput(original)) .toStrictEqual({ input1: 'Item Value 1', input2: 'Updated Input 2', 'input3': 'Item Value 3' }) - expect(promptMock).toHaveBeenCalledTimes(2) - expect(promptMock).toHaveBeenCalledWith({ - type: 'list', - name: 'action', + expect(selectMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledWith({ message: 'Object Def', choices: [ { name: 'Edit Item Name 1: Summarized Input 1', value: 'input1' }, { name: 'Edit Item Name 2: Summarized Input 2', value: 'input2' }, { name: 'Edit Item Name 3: Summarized Input 3', value: 'input3' }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Finish editing Object Def.', value: finishAction }, { name: 'Cancel', value: cancelAction }, ], default: finishAction, pageSize: inquirerPageSize, }) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Edit Item Name 1: Summarized Input 1', value: 'input1' }, { name: 'Edit Item Name 2: Summarized Input 2', value: 'input2' }, @@ -254,7 +256,7 @@ describe('objectDef', () => { input1SummarizeForEditMock.mockReturnValue('Summarized Input 1') input2SummarizeForEditMock.mockReturnValue('Summarized Input 2') input4SummarizeForEditMock.mockReturnValue('Summarized Input 4') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) const original = { prop1: 'Input Value 1', @@ -263,16 +265,14 @@ describe('objectDef', () => { } expect(await def.updateFromUserInput(original)).toStrictEqual(original) - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith({ - type: 'list', - name: 'action', + expect(selectMock).toHaveBeenCalledTimes(1) + expect(selectMock).toHaveBeenCalledWith({ message: 'Object Def', choices: [ { name: 'Edit Item Name 1: Summarized Input 1', value: 'prop1' }, { name: 'Edit Item Name 2: Summarized Input 2', value: 'prop2' }, { name: 'Edit Item Name 4: Summarized Input 4', value: 'prop4' }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Finish editing Object Def.', value: finishAction }, { name: 'Cancel', value: cancelAction }, ], @@ -289,9 +289,9 @@ describe('objectDef', () => { }), }) - promptMock.mockResolvedValueOnce({ action: 'objectProp.prop2' }) + selectMock.mockResolvedValueOnce('objectProp.prop2') input2UpdateFromUserInputMock.mockResolvedValueOnce('Updated Value 2') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) input1SummarizeForEditMock.mockReturnValue('Summarized Input 1') input2SummarizeForEditMock.mockReturnValue('Summarized Input 2') @@ -302,8 +302,8 @@ describe('objectDef', () => { { prop1: 'Item Value 1', objectProp: { prop2: 'Updated Value 2' } }, ) - expect(promptMock).toHaveBeenCalledTimes(2) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Edit Item Name 1: Summarized Input 1', value: 'prop1' }, { name: 'Edit Item Name 2: Summarized Input 2', value: 'objectProp.prop2' }, @@ -322,11 +322,11 @@ describe('objectDef', () => { }, { rollup: false, summarizeForEdit: () => 'Nested Object Summary' }), }) - promptMock.mockResolvedValueOnce({ action: 'objectProp' }) // outer object - promptMock.mockResolvedValueOnce({ action: 'prop2' }) // nested object + selectMock.mockResolvedValueOnce('objectProp') // outer object + selectMock.mockResolvedValueOnce('prop2') // nested object input2UpdateFromUserInputMock.mockResolvedValueOnce('Updated Value 2') - promptMock.mockResolvedValueOnce({ action: finishAction }) // leave nested - promptMock.mockResolvedValueOnce({ action: finishAction }) // finish outer + selectMock.mockResolvedValueOnce(finishAction) // leave nested + selectMock.mockResolvedValueOnce(finishAction) // finish outer input1SummarizeForEditMock.mockReturnValue('Summarized Input 1') input2SummarizeForEditMock.mockReturnValue('Summarized Input 2') @@ -337,14 +337,14 @@ describe('objectDef', () => { { prop1: 'Item Value 1', objectProp: { prop2: 'Updated Value 2' } }, ) - expect(promptMock).toHaveBeenCalledTimes(4) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(4) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Edit Item Name 1: Summarized Input 1', value: 'prop1' }, { name: 'Edit Nested Object: Nested Object Summary', value: 'objectProp' }, ]), })) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Edit Item Name 2: Summarized Input 2', value: 'prop2' }, ]), @@ -355,14 +355,14 @@ describe('objectDef', () => { input1SummarizeForEditMock.mockReturnValue('Summarized Input 1') input2SummarizeForEditMock.mockReturnValue('Summarized Input 2') input3SummarizeForEditMock.mockReturnValue('Summarized Input 3') - promptMock.mockResolvedValueOnce({ action: 'input2' }) + selectMock.mockResolvedValueOnce('input2') input2UpdateFromUserInputMock.mockResolvedValueOnce('Updated Input 2') - promptMock.mockResolvedValueOnce({ action: cancelAction }) + selectMock.mockResolvedValueOnce(cancelAction) const original = { input1: 'Item Value 1', input2: 'Item Value 2', 'input3': 'Item Value 3' } expect(await threeInputDef.updateFromUserInput(original)).toStrictEqual(cancelAction) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(input1UpdateFromUserInputMock).toHaveBeenCalledTimes(0) expect(input2UpdateFromUserInputMock).toHaveBeenCalledTimes(1) @@ -374,15 +374,15 @@ describe('objectDef', () => { }) it('includes help option when helpText is supplied', async () => { - promptMock.mockResolvedValueOnce({ action: helpAction }) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(helpAction) + selectMock.mockResolvedValueOnce(finishAction) const def = objectDef('Object Def', simpleInputDefsByProperty, { helpText: 'help text' }) const original = { name: 'Thing Name' } expect(await def.updateFromUserInput(original)).toStrictEqual({ name: 'Thing Name' }) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(consoleLogSpy).toHaveBeenCalledTimes(1) expect(consoleLogSpy).toHaveBeenCalledWith('\nhelp text\n') }) @@ -393,15 +393,15 @@ describe('objectDef', () => { it('logs undefined for help text in unexpected circumstance', async () => { // Here is where we mock something that can't happen since `helpAction` isn't included // when no help text is supplied. - promptMock.mockResolvedValueOnce({ action: helpAction }) - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(helpAction) + selectMock.mockResolvedValueOnce(finishAction) const def = objectDef('Object Def', simpleInputDefsByProperty) const original = { name: 'Thing Name' } expect(await def.updateFromUserInput(original)).toStrictEqual({ name: 'Thing Name' }) - expect(promptMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledTimes(2) expect(consoleLogSpy).toHaveBeenCalledTimes(1) expect(consoleLogSpy).toHaveBeenCalledWith('\nundefined\n') }) @@ -410,9 +410,9 @@ describe('objectDef', () => { input1SummarizeForEditMock.mockReturnValue('Summarized Input 1') input2SummarizeForEditMock.mockReturnValue('Summarized Input 2') input3SummarizeForEditMock.mockReturnValue('Summarized Input 3') - promptMock.mockResolvedValueOnce({ action: 'input2' }) + selectMock.mockResolvedValueOnce('input2') input2UpdateFromUserInputMock.mockResolvedValueOnce('Updated Input 2') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) const inputDef = objectDef('Object Def', { input1: input1DefMock, @@ -424,23 +424,21 @@ describe('objectDef', () => { expect(await inputDef.updateFromUserInput(original)) .toStrictEqual({ input1: 'Item Value 1', input2: 'Updated Input 2', 'input3': 'Item Value 3' }) - expect(promptMock).toHaveBeenCalledTimes(2) - expect(promptMock).toHaveBeenCalledWith({ - type: 'list', - name: 'action', + expect(selectMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledWith({ message: 'Object Def', choices: [ { name: 'Edit Item Name 1: Summarized Input 1', value: 'input1' }, { name: 'Edit Item Name 2: Summarized Input 2', value: 'input2' }, { name: 'Edit Item Name 3: Summarized Input 3', value: 'input3' }, - expect.any(inquirer.Separator), + expect.any(Separator), { name: 'Finish editing Object Def.', value: finishAction }, { name: 'Cancel', value: cancelAction }, ], default: finishAction, pageSize: inquirerPageSize, }) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Edit Item Name 1: Summarized Input 1', value: 'input1' }, { name: 'Edit Item Name 2: Summarized Input 2', value: 'input2' }, @@ -472,10 +470,10 @@ describe('objectDef', () => { prop3: input3DefWithUpdateIfNeededMock, }) - promptMock.mockResolvedValueOnce({ action: 'objectProp.prop2' }) + selectMock.mockResolvedValueOnce('objectProp.prop2') input2UpdateFromUserInputMock.mockResolvedValueOnce('Updated Prop 2 Value') updateIfNeededMock.mockResolvedValueOnce('Updated 3 Value') - promptMock.mockResolvedValueOnce({ action: finishAction }) + selectMock.mockResolvedValueOnce(finishAction) input1SummarizeForEditMock.mockReturnValue('Summarized Input 1') input2SummarizeForEditMock.mockReturnValue('Summarized Input 2') @@ -486,8 +484,8 @@ describe('objectDef', () => { { prop1: 'Prop 1 Value', objectProp: { prop2: 'Updated Prop 2 Value' }, prop3: 'Updated 3 Value' }, ) - expect(promptMock).toHaveBeenCalledTimes(2) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([ { name: 'Edit Item Name 1: Summarized Input 1', value: 'prop1' }, { name: 'Edit Item Name 2: Summarized Input 2', value: 'objectProp.prop2' }, diff --git a/src/__tests__/lib/item-input/select.test.ts b/src/__tests__/lib/item-input/select.test.ts index 33cc7a1f..a17e69e8 100644 --- a/src/__tests__/lib/item-input/select.test.ts +++ b/src/__tests__/lib/item-input/select.test.ts @@ -1,6 +1,6 @@ import { jest } from '@jest/globals' -import inquirer from 'inquirer' +import { type select, Separator } from '@inquirer/prompts' import type { clipToMaximum, stringFromUnknown } from '../../../lib/util.js' import { @@ -12,12 +12,10 @@ import { import type { SelectDefOptions } from '../../../lib/item-input/select.js' -const promptMock = jest.fn() -jest.unstable_mockModule('inquirer', () => ({ - default: { - prompt: promptMock, - Separator: inquirer.Separator, - }, +const selectMock = jest.fn() +jest.unstable_mockModule('@inquirer/prompts', () => ({ + select: selectMock, + Separator, })) const clipToMaximumMock = jest.fn().mockReturnValue('clipped') @@ -44,19 +42,18 @@ describe('selectDef', () => { describe('buildFromUserInput', () => { it('handles string values for choices', async () => { - promptMock.mockResolvedValueOnce({ selection: 'item 2' }) + selectMock.mockResolvedValueOnce('item 2') const def = selectDef('Selected Thing', ['item 1', 'item 2', 'item 3']) expect(await def.buildFromUserInput()).toBe('item 2') - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ - type: 'list', + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Select Selected Thing.', choices: [ { name: 'item 1', value: 'item 1' }, { name: 'item 2', value: 'item 2' }, { name: 'item 3', value: 'item 3' }, - expect.any(inquirer.Separator), + expect.any(Separator), cancelOption, ], default: 0, @@ -64,19 +61,18 @@ describe('selectDef', () => { }) it('handles number values for choices', async () => { - promptMock.mockResolvedValueOnce({ selection: 300 }) + selectMock.mockResolvedValueOnce(300) const def = selectDef('Selected Thing', [100, 200, 300]) expect(await def.buildFromUserInput()).toBe(300) - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ - type: 'list', + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Select Selected Thing.', choices: [ { name: '100', value: 100 }, { name: '200', value: 200 }, { name: '300', value: 300 }, - expect.any(inquirer.Separator), + expect.any(Separator), cancelOption, ], default: 0, @@ -84,18 +80,17 @@ describe('selectDef', () => { }) it('handles complex values for choices', async () => { - promptMock.mockResolvedValueOnce({ selection: 'segundo' }) + selectMock.mockResolvedValueOnce('segundo') const def = selectDef('Selected Thing', [complexItem1, complexItem2]) expect(await def.buildFromUserInput()).toBe('segundo') - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ - type: 'list', + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ message: 'Select Selected Thing.', choices: [ { name: 'First', value: 'primero' }, { name: 'Second', value: 'segundo' }, - expect.any(inquirer.Separator), + expect.any(Separator), cancelOption, ], default: 0, @@ -103,7 +98,7 @@ describe('selectDef', () => { }) it('uses specified default', async () => { - promptMock.mockResolvedValueOnce({ selection: 'primero' }) + selectMock.mockResolvedValueOnce('primero') const def = selectDef( 'Selected Thing', @@ -112,14 +107,14 @@ describe('selectDef', () => { ) expect(await def.buildFromUserInput()).toBe('primero') - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ default: 'segundo', })) }) it('displays help text when available and requested', async () => { - promptMock.mockResolvedValueOnce({ selection: helpAction }) - promptMock.mockResolvedValueOnce({ selection: 'primero' }) + selectMock.mockResolvedValueOnce(helpAction) + selectMock.mockResolvedValueOnce('primero') const def = selectDef( 'Selected Thing', @@ -128,8 +123,8 @@ describe('selectDef', () => { ) expect(await def.buildFromUserInput()).toBe('primero') - expect(promptMock).toHaveBeenCalledTimes(2) - expect(promptMock).toHaveBeenCalledWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledTimes(2) + expect(selectMock).toHaveBeenCalledWith(expect.objectContaining({ choices: expect.arrayContaining([helpOption]), })) expect(consoleLogSpy).toHaveBeenCalledWith('\nhelp text\n') @@ -138,7 +133,7 @@ describe('selectDef', () => { describe('summarizeForEdit', () => { it('uses name of complex value and clips it', async () => { - promptMock.mockResolvedValueOnce({ selection: 'primero' }) + selectMock.mockResolvedValueOnce('primero') const def = selectDef('Selected Thing', [complexItem1, complexItem2]) await def.buildFromUserInput() @@ -178,7 +173,7 @@ describe('selectDef', () => { // Shares code from buildFromUserInput so much of this is tested there. it('uses original for default', async () => { - promptMock.mockResolvedValueOnce({ selection: 'primero' }) + selectMock.mockResolvedValueOnce('primero') const def = selectDef( 'Selected Thing', @@ -187,7 +182,7 @@ describe('selectDef', () => { ) expect(await def.updateFromUserInput('segundo')).toBe('primero') - expect(promptMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ + expect(selectMock).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ default: 'segundo', })) }) diff --git a/src/lib/command/util/apps-input-primitives.ts b/src/lib/command/util/apps-input-primitives.ts index 5300c270..89074214 100644 --- a/src/lib/command/util/apps-input-primitives.ts +++ b/src/lib/command/util/apps-input-primitives.ts @@ -1,5 +1,5 @@ import { localhostOrHTTPSValidate } from '../../validate-util.js' -import { arrayDef, checkboxDef, stringDef } from '../../item-input/index.js' +import { arrayDef, CheckboxChoice, checkboxDef, stringDef } from '../../item-input/index.js' const availableScopes = [ @@ -24,7 +24,7 @@ export const oauthAppScopeDef = checkboxDef('Scopes', availableScopes, { 'To determine which scopes you need for the application, see documentation for the' + ' individual endpoints you will use in your app:\n' + ' https://developer.smartthings.com/docs/api/public/', - validate: (chosen: string[]) => chosen.length > 0 || 'At least one scope is required.', + validate: (chosen: readonly CheckboxChoice[]) => chosen.length > 0 || 'At least one scope is required.', }) const redirectUriHelpText = 'More information on redirect URIs can be found at:\n' + diff --git a/src/lib/item-input/array.ts b/src/lib/item-input/array.ts index 687d29f0..f7c5a9bc 100644 --- a/src/lib/item-input/array.ts +++ b/src/lib/item-input/array.ts @@ -1,4 +1,6 @@ -import inquirer, { CheckboxQuestion, ChoiceCollection, DistinctChoice } from 'inquirer' +import { inspect } from 'node:util' + +import { checkbox, select, Separator } from '@inquirer/prompts' import { clipToMaximum, stringFromUnknown } from '../util.js' @@ -8,6 +10,8 @@ import { cancelAction, CancelAction, cancelOption, + type CheckboxChoice, + type Choice, deleteAction, deleteOption, editAction, @@ -17,7 +21,7 @@ import { finishOption, helpAction, helpOption, - InputDefinition, + type InputDefinition, inquirerPageSize, maxItemValueLength, uneditable, @@ -78,19 +82,17 @@ export function arrayDef(name: string, itemDef: InputDefinition, options?: const editItem = async (itemDef: InputDefinition, list: T[], index: number, context: unknown[]): Promise => { const currentValue = itemSummary(list[index]) - const choices: ChoiceCollection = [editOption(currentValue)] + const choices: Choice[] = [editOption(currentValue)] if (list.length > minItems) { choices.push(deleteOption(currentValue)) } choices.push(cancelOption) - const action = (await inquirer.prompt({ - type: 'list', - name: 'action', + const action = await select({ message: `What do you want to do with ${currentValue}?`, choices, default: editAction, - })).action + }) if (action === editAction) { const response = await itemDef.updateFromUserInput(list[index], context) @@ -109,13 +111,13 @@ export function arrayDef(name: string, itemDef: InputDefinition, options?: const editList = async (list: T[], context: unknown[]): Promise => { // eslint-disable-next-line no-constant-condition while (true) { - const choices: ChoiceCollection = list.map((item, index) => ({ + const choices: (Choice)[] = list.map((item, index) => ({ name: `Edit ${itemSummary(item)}.`, value: index, })) if (choices.length > 0) { - choices.push(new inquirer.Separator()) + choices.push(new Separator()) } if (options?.helpText) { @@ -129,14 +131,12 @@ export function arrayDef(name: string, itemDef: InputDefinition, options?: } choices.push(cancelOption) - const action = (await inquirer.prompt({ - type: 'list', - name: 'action', + const action = await select({ message: `Add or edit ${name}.`, choices, default: list.length >= minItems ? finishAction : addAction, pageSize: inquirerPageSize, - })).action + }) if (action === addAction) { const newValue = await itemDef.buildFromUserInput([[...list], ...context]) @@ -152,7 +152,7 @@ export function arrayDef(name: string, itemDef: InputDefinition, options?: } else if (action === cancelAction) { return cancelAction } else { - throw Error(`unexpected state in arrayDef; action = ${JSON.stringify(action)}`) + throw Error(`unexpected state in arrayDef; action = ${inspect(action)}`) } } } @@ -183,6 +183,12 @@ export function arrayDef(name: string, itemDef: InputDefinition, options?: return { name, buildFromUserInput, summarizeForEdit, updateFromUserInput } } +/** + * Simplified version of type required by `inquirer` for checkbox validation function, which + * inquirer does not export. + */ +export type CheckboxValidateFunction = (choices: readonly CheckboxChoice[]) => true | string + export type CheckboxDefOptions = { /** * A summary of the object to display to the user in a list. @@ -192,7 +198,7 @@ export type CheckboxDefOptions = { */ summarizeForEdit?: (item: T[], context?: unknown[]) => string - validate?: (item: T[]) => string | true + validate?: CheckboxValidateFunction default?: T[] @@ -205,34 +211,35 @@ type ComplexCheckboxDefItem = { } export type CheckboxDefItem = T extends string ? string | ComplexCheckboxDefItem : ComplexCheckboxDefItem -export function checkboxDef(name: string, items: CheckboxDefItem[], options?: CheckboxDefOptions): InputDefinition { +export function checkboxDef( + name: string, + items: CheckboxDefItem[], + options?: CheckboxDefOptions, +): InputDefinition { const editValues = async (values: T[]): Promise => { // We can't add help to the inquirer `checkbox` so, at least for now, we'll display // the help before we display the checkbox. if (options?.helpText) { console.log(`\n${options.helpText}\n`) } - const choices: ChoiceCollection = items.map(item => { + const choices: CheckboxChoice[] = items.map(item => { const value = typeof item === 'string' ? item as T : item.value + const retVal: CheckboxChoice = typeof item === 'object' + ? { ...item } + : ({ name: item, value }) if (values.includes(value)) { - const retVal: DistinctChoice & { checked?: boolean } = typeof item === 'object' ? { ...item } : ({ name: item, value: item }) retVal.checked = true - return retVal } - return item + return retVal }) - const question: CheckboxQuestion = { - type: 'checkbox', - name: 'values', + const question = { message: `Select ${name}.`, choices, default: finishAction, pageSize: inquirerPageSize, + validate: options?.validate, } - if (options?.validate) { - question.validate = options.validate - } - const updatedValues = (await inquirer.prompt(question)).values + const updatedValues = await checkbox(question) return updatedValues as T[] } diff --git a/src/lib/item-input/command-helpers.ts b/src/lib/item-input/command-helpers.ts index b35db45f..8b72caf0 100644 --- a/src/lib/item-input/command-helpers.ts +++ b/src/lib/item-input/command-helpers.ts @@ -1,20 +1,21 @@ -import inquirer, { ChoiceCollection } from 'inquirer' +import { select } from '@inquirer/prompts' -import { jsonFormatter, OutputFormatter, yamlFormatter } from '../command/output.js' import { red } from '../colors.js' -import { type SmartThingsCommand } from '../command/smartthings-command.js' +import { booleanInput } from '../user-query.js' import { cancelAction, + type Choice, editAction, editOption, finishAction, - InputDefinition, + type InputDefinition, previewJSONAction, previewYAMLAction, } from './defs.js' import { cancelCommand } from '../util.js' -import { BuildOutputFormatterFlags } from '../command/output-builder.js' -import { booleanInput } from '../user-query.js' +import { jsonFormatter, OutputFormatter, yamlFormatter } from '../command/output.js' +import { type BuildOutputFormatterFlags } from '../command/output-builder.js' +import { type SmartThingsCommand } from '../command/smartthings-command.js' export type UpdateFromUserInputOptions = { @@ -73,7 +74,7 @@ export const updateFromUserInput = async ( } const finishVerb = options.dryRun ? 'output' : (options.finishVerb ?? 'update') const finishNoun = options.finishVerb === 'create' ? 'creation' : 'update' - const choices: ChoiceCollection = [ + const choices: Choice[] = [ editOption(inputDefinition.name), { name: 'Preview JSON.', value: previewJSONAction }, { name: 'Preview YAML.', value: previewYAMLAction }, @@ -87,13 +88,7 @@ export const updateFromUserInput = async ( }, ] - const action = (await inquirer.prompt({ - type: 'list', - name: 'action', - message: 'Choose an action.', - choices, - default: finishAction, - })).action + const action = await select({ message: 'Choose an action.', choices, default: finishAction }) if (action === editAction) { const answer = await inputDefinition.updateFromUserInput(retVal) diff --git a/src/lib/item-input/defs.ts b/src/lib/item-input/defs.ts index fd6316ba..120f6056 100644 --- a/src/lib/item-input/defs.ts +++ b/src/lib/item-input/defs.ts @@ -1,6 +1,24 @@ -import { DistinctChoice } from 'inquirer' +import { Separator } from '@inquirer/prompts' +export type BaseChoice = { + value: T + name?: string + description?: string + short?: string + disabled?: boolean | string +} + +/** + * A simplified version of the type required by inquirer's `select` method which they don't export. + */ +export type Choice = BaseChoice | Separator + +/** + * A simplified version of the type required by inquirer's `checkbox` method which they don't export. + */ +export type CheckboxChoice = (BaseChoice & { checked?: boolean }) | Separator + export const uneditable = Symbol('uneditable') export type Uneditable = typeof uneditable @@ -57,7 +75,11 @@ export type InputDefinition = { * 1. computed values need to be recomputed when things they depend upon have changed * 2. when selection made for one field leads to a different set of later fields requiring input */ - updateIfNeeded?: (original: U, updatedPropertyName: string | number | symbol, context?: unknown[]) => Promise + updateIfNeeded?: ( + original: U, + updatedPropertyName: string | number | symbol, + context?: unknown[], + ) => Promise /** * Specific item types can include data here for reference outside the definition builder. @@ -86,27 +108,27 @@ export type InputDefinitionDefaultValueOrFn = T | DefaultValueFunction export const addAction = Symbol('add') export type AddAction = typeof addAction -export const addOption = (name: string): DistinctChoice => ({ name: `Add ${name}.`, value: addAction }) +export const addOption = (name: string): Choice => ({ name: `Add ${name}.`, value: addAction }) export const editAction = Symbol('edit') export type EditAction = typeof editAction -export const editOption = (name: string): DistinctChoice => ({ name: `Edit ${name}.`, value: editAction }) +export const editOption = (name: string): Choice => ({ name: `Edit ${name}.`, value: editAction }) export const deleteAction = Symbol('delete') export type DeleteAction = typeof deleteAction -export const deleteOption = (name: string): DistinctChoice => ({ name: `Delete ${name}.`, value: deleteAction }) +export const deleteOption = (name: string): Choice => ({ name: `Delete ${name}.`, value: deleteAction }) export const cancelAction = Symbol('cancel') export type CancelAction = typeof cancelAction -export const cancelOption: DistinctChoice = ({ name: 'Cancel', value: cancelAction }) +export const cancelOption: Choice = ({ name: 'Cancel', value: cancelAction }) export const finishAction = Symbol('finish') export type FinishAction = typeof finishAction -export const finishOption = (name: string): DistinctChoice => ({ name: `Finish editing ${name}.`, value: finishAction }) +export const finishOption = (name: string): Choice => ({ name: `Finish editing ${name}.`, value: finishAction }) export const helpAction = Symbol('help') export type HelpAction = typeof helpAction -export const helpOption: DistinctChoice = ({ name: 'Help', value: helpAction }) +export const helpOption: Choice = ({ name: 'Help', value: helpAction }) export const previewJSONAction = Symbol('previewJSON') export const previewYAMLAction = Symbol('previewYAML') diff --git a/src/lib/item-input/misc.ts b/src/lib/item-input/misc.ts index bab98391..39a3259f 100644 --- a/src/lib/item-input/misc.ts +++ b/src/lib/item-input/misc.ts @@ -1,10 +1,10 @@ -import inquirer, { ChoiceCollection } from 'inquirer' +import { select } from '@inquirer/prompts' import { booleanInput, - BooleanInputOptions, + type BooleanInputOptions, integerInput, - IntegerInputOptions, + type IntegerInputOptions, optionalIntegerInput, type DefaultValueOrFn, optionalStringInput, @@ -14,6 +14,7 @@ import { } from '../../lib/user-query.js' import { type CancelAction, + type Choice, type InputDefinition, type InputDefinitionDefaultValueOrFn, type InputDefinitionValidateFunction, @@ -156,19 +157,13 @@ export const listSelectionDef = (name: string, validItems: T[], option const summarizeForEdit = options?.summarizeForEdit ?? stringFromUnknown const updateFromUserInput = async (original?: T): Promise => { - const choices: ChoiceCollection = validItems.map((validItem: T) => ({ + const choices: Choice[] = validItems.map((validItem: T) => ({ name: summarizeForEdit(validItem), value: validItem, })) choices.push(cancelOption) - const chosen: T | CancelAction = (await inquirer.prompt({ - type: 'list', - name: 'chosen', - message: `Select ${name}:`, - choices, - default: original, - })).chosen + const chosen = await select({ message: `Select ${name}:`, choices, default: original }) return chosen } diff --git a/src/lib/item-input/object.ts b/src/lib/item-input/object.ts index d6e00f28..8efcf51a 100644 --- a/src/lib/item-input/object.ts +++ b/src/lib/item-input/object.ts @@ -1,14 +1,17 @@ -import inquirer, { ChoiceCollection } from 'inquirer' +import { select, Separator } from '@inquirer/prompts' import { cancelAction, - CancelAction, + type CancelAction, cancelOption, + type Choice, + type FinishAction, finishAction, finishOption, + type HelpAction, helpAction, helpOption, - InputDefinition, + type InputDefinition, inquirerPageSize, uneditable, } from './defs.js' @@ -54,8 +57,12 @@ export type ObjectItemTypeData = { const maxPropertiesForDefaultRollup = 3 -const buildPropertyChoices = (inputDefsByProperty: InputDefsByProperty, updated: T, contextForChildren: unknown[]): ChoiceCollection => { - const choices = [] as ChoiceCollection +const buildPropertyChoices = ( + inputDefsByProperty: InputDefsByProperty, + updated: T, + contextForChildren: unknown[], +): Choice[] => { + const choices: Choice[] = [] for (const propertyName in inputDefsByProperty) { const propertyInputDefinition = inputDefsByProperty[propertyName] if (!propertyInputDefinition) { @@ -145,21 +152,14 @@ export function objectDef(name: string, inputDefsByProperty: I while (true) { const contextForChildren = [{ ...updated }, ...context] const choices = buildPropertyChoices(inputDefsByProperty, updated, contextForChildren) - choices.push(new inquirer.Separator()) + choices.push(new Separator()) if (options?.helpText) { choices.push(helpOption) } choices.push(finishOption(name)) choices.push(cancelOption) - const action = (await inquirer.prompt({ - type: 'list', - name: 'action', - message: name, - choices, - default: finishAction, - pageSize: inquirerPageSize, - })).action + const action = await select({ message: name, choices, default: finishAction, pageSize: inquirerPageSize }) if (action === helpAction) { console.log(`\n${options?.helpText}\n`) diff --git a/src/lib/item-input/select.ts b/src/lib/item-input/select.ts index 73f6c3fd..1c4e2e2e 100644 --- a/src/lib/item-input/select.ts +++ b/src/lib/item-input/select.ts @@ -1,8 +1,10 @@ -import inquirer, { type ChoiceCollection } from 'inquirer' +import { select, Separator } from '@inquirer/prompts' import { type CancelAction, cancelOption, + type Choice, + type HelpAction, helpAction, helpOption, type InputDefinition, @@ -50,12 +52,12 @@ export function selectDef( const namesByValue = new Map(choices.map(choice => [valueOfChoice(choice), nameOfChoice(choice)])) const editValue = async (defaultSelection: T | undefined): Promise => { - const inquirerChoices: ChoiceCollection = choices.map(choice => ({ + const inquirerChoices: (Choice)[] = choices.map(choice => ({ name: nameOfChoice(choice), value: valueOfChoice(choice), })) - inquirerChoices.push(new inquirer.Separator()) + inquirerChoices.push(new Separator()) if (options?.helpText) { inquirerChoices.push(helpOption) } @@ -63,14 +65,12 @@ export function selectDef( // eslint-disable-next-line no-constant-condition while (true) { - const selection = (await inquirer.prompt({ - type: 'list', - name: 'selection', + const selection = await select({ message: `Select ${name}.`, choices: inquirerChoices, default: defaultSelection ?? 0, pageSize: inquirerPageSize, - })).selection + }) if (selection === helpAction) { console.log(`\n${options?.helpText}\n`)