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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ smartthings edge:channels:create --help
| devices:rename [id] [new-label] | rename a device |
| devices:status [id-or-index] | get the current status of all of a device's component's attributes |
| devices:update [id] | update a device's label and room |
| edge | edge-specific commands |
| edge:channels [id-or-index] | list all channels owned by you or retrieve a single channel |
| edge:channels:assign [driver-id] [driver-version] | assign a driver to a channel |
| edge:channels:create | create a channel |
Expand Down
10 changes: 8 additions & 2 deletions src/__tests__/commands/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type SmartThingsClient,
} from '@smartthings/core-sdk'

import type { buildEpilog } from '../../lib/help.js'
import type { APICommand, APICommandFlags } from '../../lib/command/api-command.js'
import type { outputItemOrList, outputItemOrListBuilder } from '../../lib/command/listing-io.js'
import type { CommandArgs } from '../../commands/apps.js'
Expand All @@ -22,7 +23,12 @@ import { apiCommandMocks } from '../test-lib/api-command-mock.js'
import { buildArgvMock, buildArgvMockStub } from '../test-lib/builder-mock.js'


const { apiCommandMock, apiCommandBuilderMock, apiDocsURLMock } = apiCommandMocks('../..')
const buildEpilogMock = jest.fn<typeof buildEpilog>()
jest.unstable_mockModule('../../lib/help.js', () => ({
buildEpilog: buildEpilogMock,
}))

const { apiCommandMock, apiCommandBuilderMock } = apiCommandMocks('../..')

const outputItemOrListMock = jest.fn<typeof outputItemOrList<PagedApp | AppResponse>>()
const outputItemOrListBuilderMock = jest.fn<typeof outputItemOrListBuilder>()
Expand Down Expand Up @@ -69,7 +75,7 @@ describe('builder', () => {
expect(positionalMock).toHaveBeenCalledTimes(1)
expect(optionMock).toHaveBeenCalledTimes(3)
expect(exampleMock).toHaveBeenCalledTimes(1)
expect(apiDocsURLMock).toHaveBeenCalledTimes(1)
expect(buildEpilogMock).toHaveBeenCalledTimes(1)
expect(epilogMock).toHaveBeenCalledTimes(1)
})

Expand Down
9 changes: 6 additions & 3 deletions src/__tests__/commands/edge/channels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Channel, ChannelsEndpoint } from '@smartthings/core-sdk'

import type { CommandArgs } from '../../../commands/edge/channels.js'
import type { WithOrganization } from '../../../lib/api-helpers.js'
import type { buildEpilog } from '../../../lib/help.js'
import type {
APIOrganizationCommand,
APIOrganizationCommandFlags,
Expand All @@ -18,11 +19,13 @@ import type {
} from '../../../lib/command/common-flags.js'
import type { outputItemOrList, outputItemOrListBuilder } from '../../../lib/command/listing-io.js'
import { listChannels } from '../../../lib/command/util/edge/channels.js'
import { apiCommandMocks } from '../../test-lib/api-command-mock.js'
import { buildArgvMock, buildArgvMockStub } from '../../test-lib/builder-mock.js'


const { apiDocsURLMock } = apiCommandMocks('../../..')
const buildEpilogMock = jest.fn<typeof buildEpilog>()
jest.unstable_mockModule('../../../lib/help.js', () => ({
buildEpilog: buildEpilogMock,
}))

const apiOrganizationCommandMock = jest.fn<typeof apiOrganizationCommand>()
const apiOrganizationCommandBuilderMock = jest.fn<typeof apiOrganizationCommandBuilder>()
Expand Down Expand Up @@ -83,7 +86,7 @@ describe('builder', () => {
expect(optionMock).toHaveBeenCalledTimes(3)
expect(positionalMock).toHaveBeenCalledTimes(2)
expect(exampleMock).toHaveBeenCalledTimes(1)
expect(apiDocsURLMock).toHaveBeenCalledTimes(1)
expect(buildEpilogMock).toHaveBeenCalledTimes(1)
expect(epilogMock).toHaveBeenCalledTimes(1)
})

Expand Down
10 changes: 8 additions & 2 deletions src/__tests__/commands/locations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import type { ArgumentsCamelCase, Argv } from 'yargs'

import type { Location, LocationsEndpoint, SmartThingsClient } from '@smartthings/core-sdk'

import type { buildEpilog } from '../../lib/help.js'
import type { APICommand, APICommandFlags } from '../../lib/command/api-command.js'
import type { outputItemOrList, outputItemOrListBuilder } from '../../lib/command/listing-io.js'
import type { CommandArgs } from '../../commands/locations.js'
import { apiCommandMocks } from '../test-lib/api-command-mock.js'
import { buildArgvMock, buildArgvMockStub } from '../test-lib/builder-mock.js'


const { apiCommandMock, apiCommandBuilderMock, apiDocsURLMock } = apiCommandMocks('../..')
const buildEpilogMock = jest.fn<typeof buildEpilog>()
jest.unstable_mockModule('../../lib/help.js', () => ({
buildEpilog: buildEpilogMock,
}))

const { apiCommandMock, apiCommandBuilderMock } = apiCommandMocks('../..')

const outputItemOrListMock = jest.fn<typeof outputItemOrList>()
const outputItemOrListBuilderMock = jest.fn<typeof outputItemOrListBuilder>()
Expand Down Expand Up @@ -47,7 +53,7 @@ test('builder', () => {

expect(positionalMock).toHaveBeenCalledTimes(1)
expect(exampleMock).toHaveBeenCalledTimes(1)
expect(apiDocsURLMock).toHaveBeenCalledTimes(1)
expect(buildEpilogMock).toHaveBeenCalledTimes(1)
expect(epilogMock).toHaveBeenCalledTimes(1)
})

Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/lib/command-util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { jest } from '@jest/globals'

import { type CommandModule } from 'yargs'


// Single consolidated mock command set covering all test scenarios
const noop = (): void => { /* unused */ }
const devicesStatusCommand = { command: 'devices:status', describe: 'device status', handler: noop }
const devicesUpdateCommand = { command: 'devices:update', describe: 'update device', handler: noop }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockCommands: CommandModule<object, any>[] = [
{ command: 'other:thing', describe: 'other thing', handler: noop },
{ command: 'unrelated', describe: 'unrelated', handler: noop },
devicesStatusCommand,
{ command: 'devices:history:list', describe: 'device history list', handler: noop },
devicesUpdateCommand,
{ command: 'devices:history:list:detail', describe: 'history detail', handler: noop },
{ describe: 'a command without a command', handler: noop },
{ command: ['aliased', 'alias'], describe: 'a command with more than one name', handler: noop },
]
jest.unstable_mockModule('../../commands/index.js', () => ({
commands: mockCommands,
}))

const { findTopicsAndSubcommands } = await import('../../lib/command-util.js')

describe('findTopicsAndSubcommands', () => {
it('returns no topics or sub-commands for a leaf command', () => {
expect(findTopicsAndSubcommands('devices:update')).toStrictEqual({ topics: [], subCommands: [] })
})

it('returns direct, and only direct, sub-commands and topics for devices', () => {
const result = findTopicsAndSubcommands('devices')
expect(result).toStrictEqual({
topics: ['devices::history'],
subCommands: [
{ relatedName: 'devices:status', command: devicesStatusCommand },
{ relatedName: 'devices:update', command: devicesUpdateCommand },
],
})
})
})
4 changes: 2 additions & 2 deletions src/__tests__/lib/command/api-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ const {
describe('apiDocsURL', () => {
it('produces URL', () => {
expect(apiDocsURL('getDevice'))
.toBe('For API information, see:\n\n' +
.toBe('For API information, see:\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice')
})

it('joins multiple pages with line breaks', () => {
expect(apiDocsURL('getDevice', 'getDevices'))
.toBe('For API information, see:\n\n' +
.toBe('For API information, see:\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevices')
})
Expand Down
149 changes: 149 additions & 0 deletions src/__tests__/lib/help.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { jest } from '@jest/globals'

import type { BorderConfig, getBorderCharacters, table } from 'table'
import type { findTopicsAndSubcommands } from '../../lib/command-util.js'


const borderConfig = { topLeft: 'top-left' } as BorderConfig
const getBorderCharactersMock = jest.fn<typeof getBorderCharacters>()
.mockReturnValue(borderConfig)
const tableMock = jest.fn<typeof table>()
jest.unstable_mockModule('table', () => ({
getBorderCharacters: getBorderCharactersMock,
table: tableMock,
}))

const findTopicsAndSubcommandsMock = jest.fn<typeof findTopicsAndSubcommands>()
.mockReturnValue({ topics: [], subCommands: [] })
jest.unstable_mockModule('../../lib/command-util.js', () => ({
findTopicsAndSubcommands: findTopicsAndSubcommandsMock,
}))


const { buildEpilog, apiDocsURL, itemInputHelpText } = await import('../../lib/help.js')


describe('apiDocsURL', () => {
it('builds URL stanza for single api name', () => {
const result = apiDocsURL('getDevice')
expect(result).toBe('For API information, see:\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice')
})

it('builds URL stanza for multiple api names', () => {
const result = apiDocsURL(['getDevice', 'listDevices'])
expect(result).toBe(
'For API information, see:\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice\n' +
' https://developer.smartthings.com/docs/api/public/#operation/listDevices',
)
})

it('passes through existing URLs', () => {
const result = apiDocsURL(['http://example.com/doc', 'https://example.com/ssl-doc', 'getDevice'])
expect(result).toBe(
'For API information, see:\n' +
' http://example.com/doc\n' +
' https://example.com/ssl-doc\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice',
)
})
})

describe('itemInputHelpText', () => {
it('builds help text for a single name', () => {
const result = itemInputHelpText('getDevice')
expect(result).toBe('More information can be found at:\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice')
})

it('builds help text for multiple names and URLs', () => {
const result = itemInputHelpText('getDevice', 'http://example.com/doc')
expect(result).toBe(
'More information can be found at:\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice\n' +
' http://example.com/doc',
)
})
})

describe('buildEpilog', () => {
it('returns empty string when no options provided', () => {
expect(buildEpilog({ command: 'test' })).toBe('')
})

it('includes note from note provided via `notes`', () => {
const epilog = buildEpilog({ command: 'test', notes: 'Single note' })
expect(epilog).toBe('Notes:\n Single note')
})

it('includes note from formattedNotes', () => {
expect(buildEpilog({ command: 'test', formattedNotes: 'formatted note' })).toBe('Notes:\nformatted note')
})

it('includes all notes from multiple notes provided via `notes`', () => {
expect(buildEpilog({ command: 'test', notes: ['First note', 'Second note', 'Third note'] }))
.toBe('Notes:\n First note\n Second note\n Third note')
})

it('includes notes from both notes and formattedNotes', () => {
expect(buildEpilog({ command: 'test', notes: ['note 1', 'note 2'], formattedNotes: 'formatted notes' }))
.toBe('Notes:\n note 1\n note 2\nformatted notes')
})

it('includes topics section when topics found', () => {
findTopicsAndSubcommandsMock.mockReturnValueOnce({ topics: ['test::topic'], subCommands: [] })
expect(buildEpilog({ command: 'test' })).toBe('Topics:\n test::topic')

findTopicsAndSubcommandsMock.mockReturnValueOnce({ topics: ['topic1', 'topic2'], subCommands: [] })
expect(buildEpilog({ command: 'test' })).toBe('Topics:\n topic1\n topic2')
})

it('includes apiDocs section when apiDocs provided', () => {
expect(buildEpilog({ command: 'devices', apiDocs: ['getDevice', 'listDevices'] }))
.toBe(
'For API information, see:\n' +
' https://developer.smartthings.com/docs/api/public/#operation/getDevice\n' +
' https://developer.smartthings.com/docs/api/public/#operation/listDevices',
)
})

it('includes sub-commands section when sub-commands found', () => {
tableMock.mockReturnValueOnce('sub-command table output')
const subCommands = [
{
relatedName: 'test:sub1',
command: {
describe: 'sub 1 description',
handler: () => { /* noop */ },
},
},
{
relatedName: 'test:sub2',
command: {
describe: 'sub 2 description',
handler: () => { /* noop */ },
},
},
]
findTopicsAndSubcommandsMock.mockReturnValueOnce({ topics: [], subCommands })

expect(buildEpilog({ command: 'test' })).toBe(('Sub-Commands:\nsub-command table output'))

expect(getBorderCharactersMock).toHaveBeenCalledExactlyOnceWith('void')
expect(tableMock).toHaveBeenCalledExactlyOnceWith([
[' test:sub1', 'sub 1 description' ],
[' test:sub2', 'sub 2 description' ],
], expect.objectContaining({ border: borderConfig }))

// Call this trivial function to fulfill test coverage. :-)
expect(tableMock.mock.calls[0][1]?.drawHorizontalLine?.(0, 0)).toBe(false)
})

it('joins sections correctly when multiple present', () => {
findTopicsAndSubcommandsMock.mockReturnValueOnce({ topics: ['test::topic'], subCommands: [] })

expect(buildEpilog({ command: 'test', formattedNotes: 'formatted note' }))
.toBe('Notes:\nformatted note\n\nTopics:\n test::topic')
})
})
4 changes: 2 additions & 2 deletions src/commands/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {
type AppResponse,
} from '@smartthings/core-sdk'

import { buildEpilog } from '../lib/help.js'
import { type TableFieldDefinition } from '../lib/table-generator.js'
import {
type APICommandFlags,
apiCommand,
apiCommandBuilder,
apiDocsURL,
} from '../lib/command/api-command.js'
import {
type OutputItemOrListConfig,
Expand Down Expand Up @@ -61,7 +61,7 @@ const builder = (yargs: Argv): Argv<CommandArgs> =>
['$0 apps --classification SERVICE', 'list SERVICE classification apps'],
['$0 apps --type API_ONLY', 'list API-only apps'],
])
.epilog(apiDocsURL('listApps', 'getApp'))
.epilog(buildEpilog({ command, apiDocs: ['listApps', 'getApp'] }))

const handler = async (argv: ArgumentsCamelCase<CommandArgs>): Promise<void> => {
const command = await apiCommand(argv)
Expand Down
Loading