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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 41 additions & 24 deletions src/lib/interact-for-array.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { getOutput } from './output/get-output.js'
import { promptNumber, promptSelect, promptText } from './util/prompt.js'
import {
PromptCancelledError,
promptNumber,
promptSelect,
promptText,
withBackHint,
} from './util/prompt.js'

export const interactForArray = async (
array: string[],
Expand All @@ -23,31 +29,42 @@ export const interactForArray = async (
do {
displayList()

action = await promptSelect({
message: 'Choose an action:',
choices: [
{ label: 'Add an item', value: 'add' },
{ label: 'Remove an item', value: 'remove' },
{ label: 'Finish editing', value: 'done' },
],
})

if (action === 'add') {
const newItem = await promptText({
message: 'Enter the new item:',
try {
action = await promptSelect({
message: withBackHint('Choose an action:'),
choices: [
{ label: 'Add an item', value: 'add' },
{ label: 'Remove an item', value: 'remove' },
{ label: 'Finish editing', value: 'done' },
],
})
if (newItem) {
updatedArray.push(newItem)
} catch (error) {
if (!(error instanceof PromptCancelledError)) throw error
// Dismissing the action menu finishes editing, keeping the changes.
break
}

try {
if (action === 'add') {
const newItem = await promptText({
message: withBackHint('Enter the new item:'),
})
if (newItem) {
updatedArray.push(newItem)
}
} else if (action === 'remove') {
const index = await promptNumber({
message: withBackHint('Enter the index of the item to remove:'),
validate: (value) =>
value > 0 && value <= updatedArray.length
? undefined
: 'Invalid index',
})
updatedArray.splice(index - 1, 1)
}
} else if (action === 'remove') {
const index = await promptNumber({
message: 'Enter the index of the item to remove:',
validate: (value) =>
value > 0 && value <= updatedArray.length
? undefined
: 'Invalid index',
})
updatedArray.splice(index - 1, 1)
} catch (error) {
if (!(error instanceof PromptCancelledError)) throw error
// Dismissing an inner prompt returns to the action menu.
}
} while (action !== 'done')

Expand Down
90 changes: 86 additions & 4 deletions src/lib/interact-for-blueprint-object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ import { interactForBlueprintObject } from './interact-for-blueprint-object.js'
import { createMemoryOutput } from './output/create-memory-output.js'
import { setOutput } from './output/get-output.js'
import type { ContextHelpers } from './types.js'
import { promptAutocomplete, promptSelect } from './util/prompt.js'
import type * as PromptModule from './util/prompt.js'
import {
promptAutocomplete,
PromptCancelledError,
promptSelect,
promptText,
withBackHint,
} from './util/prompt.js'

vi.mock('./util/prompt.js', () => ({
canPrompt: vi.fn(() => true),
PromptCancelledError: class extends Error {},
// Only the prompts themselves are replaced, so the real PromptCancelledError
// and withBackHint are used, as they are in production.
vi.mock('./util/prompt.js', async (importOriginal) => ({
...(await importOriginal<typeof PromptModule>()),
promptText: vi.fn(),
promptNumber: vi.fn(),
promptConfirm: vi.fn(),
Expand All @@ -20,6 +28,8 @@ vi.mock('./util/prompt.js', () => ({

beforeEach(() => {
vi.mocked(promptAutocomplete).mockClear()
vi.mocked(promptAutocomplete).mockImplementation(async () => 'done')
vi.mocked(promptText).mockReset()
// Keep the interactive chrome out of the test output.
setOutput(createMemoryOutput().output)
})
Expand Down Expand Up @@ -174,3 +184,75 @@ test.for(['custom_metadata', 'custom_metadata_has'] as const)(
).resolves.toEqual({ [name]: {} })
},
)

test('interactForBlueprintObject: dismissing the parameter menu leaves the command', async () => {
vi.mocked(promptAutocomplete).mockRejectedValueOnce(
new PromptCancelledError(),
)

await expect(
interactForBlueprintObject(
args({ device_id: 'device1' }),
ctx('interactive'),
),
).resolves.toBe('[Back]')
})

test('interactForBlueprintObject: dismissing a value prompt returns to the menu', async () => {
vi.mocked(promptAutocomplete)
.mockImplementationOnce(async () => 'name')
.mockImplementationOnce(async () => 'done')
vi.mocked(promptText).mockRejectedValueOnce(new PromptCancelledError())

// The parameter is left unset and the command still runs, rather than the
// dismissal ending the whole command.
await expect(
interactForBlueprintObject(
args({ device_id: 'device1' }),
ctx('interactive'),
),
).resolves.toEqual({ device_id: 'device1' })
expect(promptAutocomplete).toHaveBeenCalledTimes(2)
})

test('interactForBlueprintObject: dismissing a value prompt keeps an earlier value', async () => {
vi.mocked(promptAutocomplete)
.mockImplementationOnce(async () => 'name')
.mockImplementationOnce(async () => 'done')
vi.mocked(promptText).mockRejectedValueOnce(new PromptCancelledError())

await expect(
interactForBlueprintObject(
args({ device_id: 'device1', name: 'Front Door' }),
ctx('interactive'),
),
).resolves.toEqual({ device_id: 'device1', name: 'Front Door' })
})

test('interactForBlueprintObject: tells the user the parameter menu can be left', async () => {
await interactForBlueprintObject(
args({ device_id: 'device1' }),
ctx('interactive'),
)

const { message } = vi.mocked(promptAutocomplete).mock.calls[0]?.[0] as {
message: string
}
expect(message).toBe(withBackHint('[/devices/get] Parameters'))
})

test('interactForBlueprintObject: tells the user a value prompt can be left', async () => {
vi.mocked(promptAutocomplete)
.mockImplementationOnce(async () => 'name')
.mockImplementationOnce(async () => 'done')
vi.mocked(promptText).mockImplementationOnce(async () => 'Front Door')

await interactForBlueprintObject(
args({ device_id: 'device1' }),
ctx('interactive'),
)

expect(vi.mocked(promptText).mock.calls[0]?.[0]).toMatchObject({
message: withBackHint('name:'),
})
})
Loading
Loading