-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: move devices:capability-status command to yargs
Showing
20 changed files
with
616 additions
and
307 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 0 additions & 108 deletions
108
packages/cli/src/__tests__/commands/devices/capability-status.test.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
245 changes: 245 additions & 0 deletions
245
src/__tests__/commands/devices/capability-status.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
import { jest } from '@jest/globals' | ||
|
||
import type { ArgumentsCamelCase, Argv } from 'yargs' | ||
|
||
import type { | ||
CapabilityStatus, | ||
Component, | ||
Device, | ||
DevicesEndpoint, | ||
} from '@smartthings/core-sdk' | ||
|
||
import type { CommandArgs } from '../../../commands/devices/capability-status.js' | ||
import type { | ||
APICommand, | ||
apiCommand, | ||
apiCommandBuilder, | ||
apiDocsURL, | ||
} from '../../../lib/command/api-command.js' | ||
import type { stringTranslateToId } from '../../../lib/command/command-util.js' | ||
import type { | ||
CustomCommonOutputProducer, | ||
formatAndWriteItem, | ||
formatAndWriteItemBuilder, | ||
formatAndWriteList, | ||
} from '../../../lib/command/format.js' | ||
import type { BuildOutputFormatterFlags } from '../.././../lib/command/output-builder.js' | ||
import type { selectFromList } from '../../../lib/command/select.js' | ||
import type { SmartThingsCommandFlags } from '../../../lib/command/smartthings-command.js' | ||
import { | ||
chooseComponentFn, | ||
prettyPrintAttribute, | ||
} from '../../../lib/command/util/devices-util.js' | ||
import type { ChooseFunction } from '../../../lib/command/util/util-util.js' | ||
import { buildArgvMock, buildArgvMockStub } from '../../test-lib/builder-mock.js' | ||
import { | ||
mockedTableOutput, | ||
newOutputTableMock, | ||
tableGeneratorMock, | ||
tablePushMock, | ||
tableToStringMock, | ||
} from '../../test-lib/table-mock.js' | ||
import type { fatalError } from '../../../lib/util.js' | ||
|
||
|
||
const apiCommandMock = jest.fn<typeof apiCommand>() | ||
const apiCommandBuilderMock = jest.fn<typeof apiCommandBuilder>() | ||
const apiDocsURLMock = jest.fn<typeof apiDocsURL>() | ||
jest.unstable_mockModule('../../../lib/command/api-command.js', () => ({ | ||
apiCommand: apiCommandMock, | ||
apiCommandBuilder: apiCommandBuilderMock, | ||
apiDocsURL: apiDocsURLMock, | ||
})) | ||
|
||
const stringTranslateToIdMock = jest.fn<typeof stringTranslateToId>() | ||
jest.unstable_mockModule('../../../lib/command/command-util.js', () => ({ | ||
stringTranslateToId: stringTranslateToIdMock, | ||
})) | ||
|
||
const selectFromListMock = jest.fn<typeof selectFromList>() | ||
jest.unstable_mockModule('../../../lib/command/select.js', () => ({ | ||
selectFromList: selectFromListMock, | ||
})) | ||
|
||
const chooseComponentMock = jest.fn<ChooseFunction<Component>>() | ||
const chooseComponentFnMock = jest.fn<typeof chooseComponentFn>() | ||
.mockReturnValue(chooseComponentMock) | ||
const chooseDeviceMock = jest.fn<ChooseFunction<Device>>().mockResolvedValue('chosen-device-id') | ||
const prettyPrintAttributeMock = jest.fn<typeof prettyPrintAttribute>() | ||
jest.unstable_mockModule('../../../lib/command/util/devices-util.js', () => ({ | ||
chooseComponentFn: chooseComponentFnMock, | ||
chooseDevice: chooseDeviceMock, | ||
prettyPrintAttribute: prettyPrintAttributeMock, | ||
})) | ||
|
||
const formatAndWriteItemMock = jest.fn<typeof formatAndWriteItem<CapabilityStatus>>() | ||
const formatAndWriteItemBuilderMock = jest.fn<typeof formatAndWriteItemBuilder>() | ||
const formatAndWriteListMock = jest.fn<typeof formatAndWriteList<CapabilityStatus>>() | ||
jest.unstable_mockModule('../../../lib/command/format.js', () => ({ | ||
formatAndWriteItem: formatAndWriteItemMock, | ||
formatAndWriteItemBuilder: formatAndWriteItemBuilderMock, | ||
formatAndWriteList: formatAndWriteListMock, | ||
})) | ||
|
||
const fatalErrorMock = jest.fn<typeof fatalError>() | ||
jest.unstable_mockModule('../../../lib/util.js', () => ({ | ||
fatalError: fatalErrorMock, | ||
})) | ||
|
||
|
||
const { | ||
default: cmd, | ||
buildTableOutput, | ||
} = await import('../../../commands/devices/capability-status.js') | ||
|
||
test('builder', () => { | ||
const yargsMock = buildArgvMockStub<object>() | ||
const { | ||
yargsMock: apiCommandBuilderArgvMock, | ||
positionalMock, | ||
exampleMock, | ||
argvMock, | ||
epilogMock, | ||
} = buildArgvMock<SmartThingsCommandFlags, BuildOutputFormatterFlags>() | ||
|
||
apiCommandBuilderMock.mockReturnValue(apiCommandBuilderArgvMock) | ||
formatAndWriteItemBuilderMock.mockReturnValue(argvMock) | ||
|
||
const builder = cmd.builder as (yargs: Argv<object>) => Argv<CommandArgs> | ||
|
||
expect(builder(yargsMock)).toBe(argvMock) | ||
|
||
expect(apiCommandBuilderMock).toHaveBeenCalledExactlyOnceWith(yargsMock) | ||
expect(formatAndWriteItemBuilderMock).toHaveBeenCalledExactlyOnceWith(apiCommandBuilderArgvMock) | ||
expect(positionalMock).toHaveBeenCalledTimes(3) | ||
expect(exampleMock).toHaveBeenCalledOnce() | ||
expect(epilogMock).toHaveBeenCalledOnce() | ||
}) | ||
|
||
test('buildTableOutput', () => { | ||
prettyPrintAttributeMock.mockReturnValueOnce('pretty value 1') | ||
prettyPrintAttributeMock.mockReturnValueOnce('pretty value 2') | ||
const capability: CapabilityStatus = { | ||
attribute1: { value: 'value of attribute 1' }, | ||
attribute2: { value: 5 }, | ||
} | ||
|
||
expect(buildTableOutput(tableGeneratorMock, capability)).toBe(mockedTableOutput) | ||
|
||
expect(newOutputTableMock).toHaveBeenCalledExactlyOnceWith({ head: ['Attribute', 'Value'] }) | ||
expect(prettyPrintAttributeMock).toHaveBeenCalledTimes(2) | ||
expect(prettyPrintAttributeMock).toHaveBeenCalledWith(capability.attribute1) | ||
expect(prettyPrintAttributeMock).toHaveBeenCalledWith(capability.attribute2) | ||
expect(tablePushMock).toHaveBeenCalledTimes(2) | ||
expect(tablePushMock).toHaveBeenCalledWith(['attribute1', 'pretty value 1']) | ||
expect(tablePushMock).toHaveBeenCalledWith(['attribute2', 'pretty value 2']) | ||
expect(tableToStringMock).toHaveBeenCalledExactlyOnceWith() | ||
}) | ||
|
||
describe('handler', () => { | ||
const device = { | ||
deviceId: 'device-id', | ||
components: [ | ||
{ id: 'main', capabilities: [{ id: 'switch' }] }, | ||
{ id: 'sans-capabilities' }, | ||
], | ||
} as unknown as Device | ||
const capabilityStatus = { attribute: { value: 'attribute-value' } } as CapabilityStatus | ||
const apiDevicesGetMock = jest.fn<typeof DevicesEndpoint.prototype.get>() | ||
.mockResolvedValue(device) | ||
const apiDevicesGetCapabilityStatusMock = jest.fn<typeof DevicesEndpoint.prototype.getCapabilityStatus>() | ||
.mockResolvedValue(capabilityStatus) | ||
const command = { | ||
client: { | ||
devices: { | ||
get: apiDevicesGetMock, | ||
getCapabilityStatus: apiDevicesGetCapabilityStatusMock, | ||
}, | ||
}, | ||
tableGenerator: tableGeneratorMock, | ||
} as unknown as APICommand<CommandArgs> | ||
apiCommandMock.mockResolvedValue(command) | ||
const inputArgv = { | ||
deviceIdOrIndex: 'argv-id-or-index', | ||
componentId: 'argv-component-id', | ||
capabilityId: 'argv-capability-id', | ||
profile: 'default', | ||
} as ArgumentsCamelCase<CommandArgs> | ||
|
||
it('throws exception for component with no capabilities', async () => { | ||
chooseComponentMock.mockResolvedValueOnce('sans-capabilities') | ||
|
||
await expect(cmd.handler(inputArgv)).resolves.not.toThrow() | ||
|
||
expect(apiCommandMock).toHaveBeenCalledExactlyOnceWith(inputArgv) | ||
expect(chooseDeviceMock).toHaveBeenCalledExactlyOnceWith( | ||
command, | ||
'argv-id-or-index', | ||
{ allowIndex: true }, | ||
) | ||
expect(apiDevicesGetMock).toHaveBeenCalledExactlyOnceWith('chosen-device-id') | ||
expect(chooseComponentFnMock).toHaveBeenCalledExactlyOnceWith(device) | ||
expect(chooseComponentMock).toHaveBeenCalledExactlyOnceWith( | ||
command, | ||
'argv-component-id', | ||
{ autoChoose: true }, | ||
) | ||
|
||
expect(fatalErrorMock).toHaveBeenCalledExactlyOnceWith( | ||
'no capabilities found for component sans-capabilities of device chosen-device-id', | ||
) | ||
expect(formatAndWriteItemMock).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('calls formatAndWriteItem properly', async () => { | ||
chooseComponentMock.mockResolvedValueOnce('main') | ||
stringTranslateToIdMock.mockResolvedValueOnce('preselected-capability-id') | ||
selectFromListMock.mockResolvedValueOnce('chosen-capability-id') | ||
|
||
await expect(cmd.handler(inputArgv)).resolves.not.toThrow() | ||
|
||
expect(apiCommandMock).toHaveBeenCalledExactlyOnceWith(inputArgv) | ||
expect(chooseDeviceMock).toHaveBeenCalledExactlyOnceWith( | ||
command, | ||
'argv-id-or-index', | ||
{ allowIndex: true }, | ||
) | ||
expect(apiDevicesGetMock).toHaveBeenCalledExactlyOnceWith('chosen-device-id') | ||
expect(chooseComponentFnMock).toHaveBeenCalledExactlyOnceWith(device) | ||
expect(chooseComponentMock).toHaveBeenCalledExactlyOnceWith( | ||
command, | ||
'argv-component-id', | ||
{ autoChoose: true }, | ||
) | ||
expect(stringTranslateToIdMock).toHaveBeenCalledExactlyOnceWith( | ||
expect.objectContaining({ itemName: 'capability' }), | ||
'argv-capability-id', | ||
expect.any(Function), | ||
) | ||
expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( | ||
command, | ||
expect.objectContaining({ itemName: 'capability' }), | ||
{ preselectedId: 'preselected-capability-id', listItems: expect.any(Function) }, | ||
) | ||
expect(apiDevicesGetCapabilityStatusMock).toHaveBeenCalledExactlyOnceWith( | ||
'chosen-device-id', | ||
'main', | ||
'chosen-capability-id', | ||
) | ||
expect(formatAndWriteItemMock).toHaveBeenCalledExactlyOnceWith( | ||
command, | ||
{ buildTableOutput: expect.any(Function) }, | ||
capabilityStatus, | ||
) | ||
|
||
expect(fatalErrorMock).not.toHaveBeenCalled() | ||
|
||
const config = formatAndWriteItemMock.mock.calls[0][1] as | ||
CustomCommonOutputProducer<CapabilityStatus> | ||
expect(config.buildTableOutput(capabilityStatus)).toBe(mockedTableOutput) | ||
|
||
const listItems = stringTranslateToIdMock.mock.calls[0][2] | ||
|
||
expect(await listItems()).toBe(device.components?.[0].capabilities) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { jest } from '@jest/globals' | ||
|
||
import type { Table, TableGenerator } from '../../lib/table-generator.js' | ||
|
||
|
||
export const mockedTableOutput = 'table output' | ||
export const tablePushMock = jest.fn<Table['push']>() | ||
export const tableToStringMock = jest.fn<Table['toString']>().mockReturnValue(mockedTableOutput) | ||
export const tableMock: Table = { | ||
push: tablePushMock, | ||
toString: tableToStringMock, | ||
} | ||
export const newOutputTableMock = jest.fn<TableGenerator['newOutputTable']>().mockReturnValue(tableMock) | ||
export const tableGeneratorMock = { newOutputTable: newOutputTableMock } as unknown as TableGenerator |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { type ArgumentsCamelCase, type Argv, type CommandModule } from 'yargs' | ||
|
||
import { type CapabilityReference, type CapabilityStatus } from '@smartthings/core-sdk' | ||
|
||
import { | ||
apiCommand, | ||
apiCommandBuilder, | ||
apiDocsURL, | ||
type APICommandFlags, | ||
} from '../../lib/command/api-command.js' | ||
import { stringTranslateToId } from '../../lib/command/command-util.js' | ||
import { | ||
formatAndWriteItem, | ||
formatAndWriteItemBuilder, | ||
type FormatAndWriteItemFlags, | ||
} from '../../lib/command/format.js' | ||
import { selectFromList, type SelectFromListConfig } from '../../lib/command/select.js' | ||
import { | ||
chooseComponentFn, | ||
chooseDevice, | ||
prettyPrintAttribute, | ||
} from '../../lib/command/util/devices-util.js' | ||
import { type TableGenerator } from '../../lib/table-generator.js' | ||
import { fatalError } from '../../lib/util.js' | ||
|
||
|
||
export type CommandArgs = APICommandFlags & FormatAndWriteItemFlags & { | ||
deviceIdOrIndex?: string | ||
componentId?: string | ||
capabilityId?: string | ||
} | ||
|
||
const command = 'devices:capability-status [device-id-or-index] [component-id] [capability-id]' | ||
|
||
const describe = "get the current status of all of a device capability's attributes" | ||
|
||
const builder = (yargs: Argv): Argv<CommandArgs> => | ||
formatAndWriteItemBuilder(apiCommandBuilder(yargs)) | ||
.positional('device-id-or-index', | ||
{ describe: 'device id or index in list from devices command', type: 'string' }) | ||
.positional('component-id', { describe: 'component id', type: 'string' }) | ||
.positional('capability-id', { describe: 'capability id', type: 'string' }) | ||
.example([ | ||
['$0 devices:capability-status', | ||
'prompt for a device, component, and capability, then display its status'], | ||
['$0 devices:capability-status fa1eb54c-c571-405f-8817-ffb7cd2f5a9d', | ||
'prompt for a component and capability for the specified device'], | ||
['$0 devices:capability-status fa1eb54c-c571-405f-8817-ffb7cd2f5a9d main switch', | ||
'display the status for the specified device, component, and capability'], | ||
]) | ||
.epilog(apiDocsURL('getDeviceStatusByCapability')) | ||
|
||
export const buildTableOutput = ( | ||
tableGenerator: TableGenerator, | ||
capability: CapabilityStatus, | ||
): string => { | ||
const table = tableGenerator.newOutputTable({ head: ['Attribute', 'Value'] }) | ||
|
||
for (const attributeName of Object.keys(capability)) { | ||
table.push([attributeName, prettyPrintAttribute(capability[attributeName])]) | ||
} | ||
|
||
return table.toString() | ||
} | ||
|
||
const handler = async (argv: ArgumentsCamelCase<CommandArgs>): Promise<void> => { | ||
const command = await apiCommand(argv) | ||
|
||
const deviceId = await chooseDevice(command, argv.deviceIdOrIndex, { allowIndex: true }) | ||
|
||
const device = await command.client.devices.get(deviceId) | ||
const chooseComponent = chooseComponentFn(device) | ||
const componentId = await chooseComponent(command, argv.componentId, { autoChoose: true }) | ||
|
||
const component = device.components?.find(component => component.id === componentId) | ||
const capabilities = component?.capabilities | ||
if (!capabilities) { | ||
return fatalError(`no capabilities found for component ${componentId} of device ${deviceId}`) | ||
} | ||
|
||
const config: SelectFromListConfig<CapabilityReference> = { | ||
itemName: 'capability', | ||
pluralItemName: 'capabilities', | ||
primaryKeyName: 'id', | ||
sortKeyName: 'id', | ||
listTableFieldDefinitions: ['id'], | ||
} | ||
const listItems = async (): Promise<CapabilityReference[]> => capabilities | ||
const preselectedId = await stringTranslateToId(config, argv.capabilityId, listItems) | ||
const capabilityId = await selectFromList(command, config, { preselectedId, listItems }) | ||
const capabilityStatus = await command.client.devices.getCapabilityStatus( | ||
deviceId, | ||
componentId, | ||
capabilityId, | ||
) | ||
await formatAndWriteItem( | ||
command, | ||
{ buildTableOutput: data => buildTableOutput(command.tableGenerator, data) }, | ||
capabilityStatus, | ||
) | ||
} | ||
|
||
const cmd: CommandModule<object, CommandArgs> = { command, describe, builder, handler } | ||
export default cmd |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters