diff --git a/src/__tests__/lib/command/command-util.test.ts b/src/__tests__/lib/command/command-util.test.ts index 9226b47a..4ca689ce 100644 --- a/src/__tests__/lib/command/command-util.test.ts +++ b/src/__tests__/lib/command/command-util.test.ts @@ -2,7 +2,6 @@ import { jest } from '@jest/globals' import inquirer from 'inquirer' -import { ChooseOptions } from '../../../lib/command/command-util.js' import { sort } from '../../../lib/command/output.js' import { ListDataFunction, Sorting } from '../../../lib/command/basic-io.js' import { SimpleType } from '../../test-lib/simple-type.js' @@ -22,8 +21,6 @@ jest.unstable_mockModule('../../../lib/command/output.js', () => ({ const { - chooseOptionsDefaults, - chooseOptionsWithDefaults, convertToId, isIndexArgument, itemName, @@ -103,8 +100,7 @@ describe('stringTranslateToId', () => { expect(computedId).toBe('string-id-a') expect(listFunction).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledWith(list, 'num') + expect(sortMock).toHaveBeenCalledExactlyOnceWith(list, 'num') }) it('throws an error when item not found', async () => { @@ -115,8 +111,7 @@ describe('stringTranslateToId', () => { .rejects.toThrow('invalid index 4 (enter an id or index between 1 and 3 inclusive)') expect(listFunction).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledWith(list, 'num') + expect(sortMock).toHaveBeenCalledExactlyOnceWith(list, 'num') }) it('throws an error for missing primary key', async () => { @@ -127,8 +122,7 @@ describe('stringTranslateToId', () => { .rejects.toThrow('did not find key str in data') expect(listFunction).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledWith(list, 'num') + expect(sortMock).toHaveBeenCalledExactlyOnceWith(list, 'num') }) it('throws an error for invalid type for primary key', async () => { @@ -139,8 +133,7 @@ describe('stringTranslateToId', () => { .rejects.toThrow('invalid type number for primary key str in {"str":3,"num":5}') expect(listFunction).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledTimes(1) - expect(sortMock).toHaveBeenCalledWith(list, 'num') + expect(sortMock).toHaveBeenCalledExactlyOnceWith(list, 'num') }) }) @@ -175,8 +168,7 @@ describe('stringGetIdFromUser', () => { expect(chosenId).toBe('string-id-a') - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith({ + expect(promptMock).toHaveBeenCalledExactlyOnceWith({ type: 'input', name: 'itemIdOrIndex', message: 'Enter id or index', validate: expect.anything(), }) @@ -192,8 +184,7 @@ describe('stringGetIdFromUser', () => { expect(chosenId).toBe('string-id-a') - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith({ + expect(promptMock).toHaveBeenCalledExactlyOnceWith({ type: 'input', name: 'itemIdOrIndex', message: 'Enter id or index', validate: expect.anything(), }) @@ -215,8 +206,7 @@ describe('stringGetIdFromUser', () => { expect(chosenId).toBe('string-id-a') - expect(promptMock).toHaveBeenCalledTimes(1) - expect(promptMock).toHaveBeenCalledWith({ + expect(promptMock).toHaveBeenCalledExactlyOnceWith({ type: 'input', name: 'itemIdOrIndex', message: 'give me an id', validate: expect.anything(), }) @@ -225,29 +215,3 @@ describe('stringGetIdFromUser', () => { expect(validateFunction('string-id-a')).toBe(true) }) }) - -describe('chooseOptionsWithDefaults', () => { - it('uses defaults with undefined input', () => { - expect(chooseOptionsWithDefaults(undefined)).toStrictEqual(chooseOptionsDefaults()) - }) - - it('uses defaults with empty input', () => { - expect(chooseOptionsWithDefaults({})).toStrictEqual(chooseOptionsDefaults()) - }) - - it('input overrides default', () => { - const optionsDifferentThanDefault = { - allowIndex: true, - verbose: true, - useConfigDefault: true, - autoChoose: true, - } - expect(chooseOptionsWithDefaults(optionsDifferentThanDefault)) - .toEqual(optionsDifferentThanDefault) - }) - - it('passes on other values unchanged', () => { - expect(chooseOptionsWithDefaults({ someOtherKey: 'some other value' } as Partial>)) - .toEqual(expect.objectContaining({ someOtherKey: 'some other value' })) - }) -}) diff --git a/src/__tests__/lib/command/util/apps-util.test.ts b/src/__tests__/lib/command/util/apps-util.test.ts index 32ee4324..ffc5d212 100644 --- a/src/__tests__/lib/command/util/apps-util.test.ts +++ b/src/__tests__/lib/command/util/apps-util.test.ts @@ -2,33 +2,36 @@ import { jest } from '@jest/globals' import { AppResponse, AppsEndpoint, AppType, PagedApp, SmartThingsClient } from '@smartthings/core-sdk' -import { PropertyTableFieldDefinition, Table, TableGenerator, ValueTableFieldDefinition } from '../../../../lib/table-generator.js' -import { APICommand } from '../../../../lib/command/api-command.js' import { - chooseOptionsDefaults, + PropertyTableFieldDefinition, + Table, + TableGenerator, + ValueTableFieldDefinition, +} from '../../../../lib/table-generator.js' +import { stringTranslateToId } from '../../../../lib/command/command-util.js' +import { + createChooseFn, + type ChooseFunction, chooseOptionsWithDefaults, - stringTranslateToId, -} from '../../../../lib/command/command-util.js' -import { selectFromList, SelectFromListFlags } from '../../../../lib/command/select.js' +} from '../../../../lib/command/util/util-util.js' -const chooseOptionsWithDefaultsMock = jest.fn() const stringTranslateToIdMock = jest.fn() jest.unstable_mockModule('../../../../lib/command/command-util.js', () => ({ - chooseOptionsDefaults, - chooseOptionsWithDefaults: chooseOptionsWithDefaultsMock, stringTranslateToId: stringTranslateToIdMock, })) -const selectFromListMock = jest.fn() -jest.unstable_mockModule('../../../../lib/command/select.js', () => ({ - selectFromList: selectFromListMock, +const createChooseFnMock = jest.fn>() +const chooseOptionsWithDefaultsMock = jest.fn() +jest.unstable_mockModule('../../../../lib/command/util/util-util.js', () => ({ + chooseOptionsWithDefaults: chooseOptionsWithDefaultsMock, + createChooseFn: createChooseFnMock, })) const { buildTableOutput, - chooseApp, + chooseAppFn, hasSubscription, isWebhookSmartApp, shortARNorURL, @@ -139,29 +142,32 @@ describe('tableFieldDefinitions functions', () => { }) }) -test('chooseApp uses correct endpoint to list apps', async () => { - selectFromListMock.mockResolvedValue('selected-app-id') - chooseOptionsWithDefaultsMock.mockReturnValue(chooseOptionsDefaults()) +test('chooseAppFn uses correct endpoint to list apps', async () => { + const chooseAppMock = jest.fn>() + createChooseFnMock.mockReturnValueOnce(chooseAppMock) - const apiAppsListMock = jest.fn() - const command = { - client: { - apps: { - list: apiAppsListMock, - }, - }, - } as unknown as APICommand + const chooseApp = chooseAppFn() + + expect(chooseApp).toBe(chooseAppMock) - expect(await chooseApp(command)).toBe('selected-app-id') + expect(createChooseFnMock).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ itemName: 'app' }), + expect.any(Function), + ) - const listItems = selectFromListMock.mock.calls[0][2].listItems - const appsList = [{ appId: 'listed-app-id' } as PagedApp] - apiAppsListMock.mockResolvedValueOnce(appsList) + const appList = [{ appId: 'listed-app-id' } as PagedApp] + const apiAppsListMock = jest.fn() + .mockResolvedValueOnce(appList) + const listItems = createChooseFnMock.mock.calls[0][1] + const client = { + apps: { + list: apiAppsListMock, + }, + } as unknown as SmartThingsClient - expect(await listItems()).toBe(appsList) + expect(await listItems(client)).toBe(appList) - expect(apiAppsListMock).toHaveBeenCalledTimes(1) - expect(apiAppsListMock).toHaveBeenCalledWith() + expect(apiAppsListMock).toHaveBeenCalledExactlyOnceWith() }) describe('buildTableOutput', () => { @@ -183,10 +189,8 @@ describe('buildTableOutput', () => { expect(newOutputTableMock).toHaveBeenCalledWith( expect.objectContaining({ head: ['Key', 'Value'] }), ) - expect(pushMock).toHaveBeenCalledTimes(1) - expect(pushMock).toHaveBeenCalledWith(['setting', 'setting value']) - expect(toStringMock).toHaveBeenCalledTimes(1) - expect(toStringMock).toHaveBeenCalledWith() + expect(pushMock).toHaveBeenCalledExactlyOnceWith(['setting', 'setting value']) + expect(toStringMock).toHaveBeenCalledExactlyOnceWith() }) }) @@ -202,8 +206,7 @@ describe('verboseApps', () => { expect(await verboseApps(client, options)) - expect(listMock).toHaveBeenCalledTimes(1) - expect(listMock).toHaveBeenCalledWith(options) + expect(listMock).toHaveBeenCalledExactlyOnceWith(options) expect(getMock).toHaveBeenCalledTimes(0) }) @@ -219,8 +222,7 @@ describe('verboseApps', () => { expect(await verboseApps(client, {})) - expect(listMock).toHaveBeenCalledTimes(1) - expect(listMock).toHaveBeenCalledWith({}) + expect(listMock).toHaveBeenCalledExactlyOnceWith({}) expect(getMock).toHaveBeenCalledTimes(2) expect(getMock).toHaveBeenCalledWith('paged-app-1-id') expect(getMock).toHaveBeenCalledWith('paged-app-2-id') diff --git a/src/__tests__/lib/command/util/locations-util.test.ts b/src/__tests__/lib/command/util/locations-util.test.ts index 1ee58eaa..18686aa2 100644 --- a/src/__tests__/lib/command/util/locations-util.test.ts +++ b/src/__tests__/lib/command/util/locations-util.test.ts @@ -1,52 +1,50 @@ import { jest } from '@jest/globals' -import { selectFromList, SelectFromListFlags } from '../../../../lib/command/select.js' -import { APICommand } from '../../../../lib/command/api-command.js' -import { LocationItem, LocationsEndpoint } from '@smartthings/core-sdk' +import { LocationItem, LocationsEndpoint, SmartThingsClient } from '@smartthings/core-sdk' +import { stringTranslateToId } from '../../../../lib/command/command-util.js' import { - chooseOptionsDefaults, - chooseOptionsWithDefaults, - stringTranslateToId, -} from '../../../../lib/command/command-util.js' + createChooseFn, + type ChooseFunction, +} from '../../../../lib/command/util/util-util.js' -const chooseOptionsWithDefaultsMock = jest.fn() const stringTranslateToIdMock = jest.fn() jest.unstable_mockModule('../../../../lib/command/command-util.js', () => ({ - chooseOptionsDefaults, - chooseOptionsWithDefaults: chooseOptionsWithDefaultsMock, stringTranslateToId: stringTranslateToIdMock, })) -const selectFromListMock = jest.fn() -jest.unstable_mockModule('../../../../lib/command/select.js', () => ({ - selectFromList: selectFromListMock, +const createChooseFnMock = jest.fn>() +jest.unstable_mockModule('../../../../lib/command/util/util-util.js', () => ({ + createChooseFn: createChooseFnMock, })) -const { chooseLocation } = await import('../../../../lib/command/util/locations-util.js') +const { chooseLocationFn } = await import('../../../../lib/command/util/locations-util.js') -test('chooseLocation uses correct endpoint to list locations', async () => { - selectFromListMock.mockResolvedValue('selected-location-id') - chooseOptionsWithDefaultsMock.mockReturnValue(chooseOptionsDefaults()) +test('chooseLocationFn uses correct endpoint to list locations', async () => { + const chooseLocationMock = jest.fn>() + createChooseFnMock.mockReturnValueOnce(chooseLocationMock) - const apiLocationsListMock = jest.fn() - const command = { - client: { - locations: { - list: apiLocationsListMock, - }, - }, - } as unknown as APICommand + const chooseLocation = chooseLocationFn() + + expect(chooseLocation).toBe(chooseLocationMock) - expect(await chooseLocation(command)).toBe('selected-location-id') + expect(createChooseFnMock).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ itemName: 'location' }), + expect.any(Function), + ) - const listItems = selectFromListMock.mock.calls[0][2].listItems const locationList = [{ locationId: 'listed-location-id' } as LocationItem] - apiLocationsListMock.mockResolvedValueOnce(locationList) + const apiLocationsListMock = jest.fn() + .mockResolvedValueOnce(locationList) + const listItems = createChooseFnMock.mock.calls[0][1] + const client = { + locations: { + list: apiLocationsListMock, + }, + } as unknown as SmartThingsClient - expect(await listItems()).toBe(locationList) + expect(await listItems(client)).toBe(locationList) - expect(apiLocationsListMock).toHaveBeenCalledTimes(1) - expect(apiLocationsListMock).toHaveBeenCalledWith() + expect(apiLocationsListMock).toHaveBeenCalledExactlyOnceWith() }) diff --git a/src/__tests__/lib/command/util/util-util.test.ts b/src/__tests__/lib/command/util/util-util.test.ts index 642b18db..eb0b8f5a 100644 --- a/src/__tests__/lib/command/util/util-util.test.ts +++ b/src/__tests__/lib/command/util/util-util.test.ts @@ -1,23 +1,16 @@ import { jest } from '@jest/globals' -import { selectFromList, SelectFromListConfig, SelectFromListFlags } from '../../../../lib/command/select.js' -import { APICommand } from '../../../../lib/command/api-command.js' -import { SmartThingsClient } from '@smartthings/core-sdk' -import { ListDataFunction } from '../../../../lib/command/basic-io.js' -import { - ChooseOptions, - chooseOptionsDefaults, - chooseOptionsWithDefaults, - stringTranslateToId, -} from '../../../../lib/command/command-util.js' -import { SimpleType } from '../../../test-lib/simple-type.js' +import { selectFromList, type SelectFromListConfig, type SelectFromListFlags } from '../../../../lib/command/select.js' +import { type APICommand } from '../../../../lib/command/api-command.js' +import { type SmartThingsClient } from '@smartthings/core-sdk' +import { type ListDataFunction } from '../../../../lib/command/basic-io.js' +import { stringTranslateToId } from '../../../../lib/command/command-util.js' +import { type ChooseOptions } from '../../../../lib/command/util/util-util.js' +import { type SimpleType } from '../../../test-lib/simple-type.js' -const chooseOptionsWithDefaultsMock = jest.fn() const stringTranslateToIdMock = jest.fn() jest.unstable_mockModule('../../../../lib/command/command-util.js', () => ({ - chooseOptionsDefaults, - chooseOptionsWithDefaults: chooseOptionsWithDefaultsMock, stringTranslateToId: stringTranslateToIdMock, })) @@ -27,7 +20,37 @@ jest.unstable_mockModule('../../../../lib/command/select.js', () => ({ })) -const { createChooseFn } = await import('../../../../lib/command/util/util-util.js') +const { + chooseOptionsDefaults, + chooseOptionsWithDefaults, + createChooseFn, +} = await import('../../../../lib/command/util/util-util.js') + +describe('chooseOptionsWithDefaults', () => { + it('uses defaults with undefined input', () => { + expect(chooseOptionsWithDefaults(undefined)).toStrictEqual(chooseOptionsDefaults()) + }) + + it('uses defaults with empty input', () => { + expect(chooseOptionsWithDefaults({})).toStrictEqual(chooseOptionsDefaults()) + }) + + it('input overrides default', () => { + const optionsDifferentThanDefault = { + allowIndex: true, + verbose: true, + useConfigDefault: true, + autoChoose: true, + } + expect(chooseOptionsWithDefaults(optionsDifferentThanDefault)) + .toEqual(optionsDifferentThanDefault) + }) + + it('passes on other values unchanged', () => { + expect(chooseOptionsWithDefaults({ someOtherKey: 'some other value' } as Partial>)) + .toEqual(expect.objectContaining({ someOtherKey: 'some other value' })) + }) +}) describe('createChooseFn', () => { const item1: SimpleType = { str: 'string-id-a', num: 5 } @@ -49,7 +72,6 @@ describe('createChooseFn', () => { stringTranslateToIdMock.mockResolvedValue('translated-simple-type-id') selectFromListMock.mockResolvedValue('selected-simple-type-id') - chooseOptionsWithDefaultsMock.mockReturnValue(chooseOptionsDefaults()) const command = { client: { notAReal: 'SmartThingsClient' }, @@ -66,8 +88,12 @@ describe('createChooseFn', () => { expect(await chooseSimpleType(command, undefined, opts)).toBe('selected-simple-type-id') expect(listItemsMock).toHaveBeenCalledTimes(0) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledTimes(1) - expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledWith(opts) + expect(stringTranslateToIdMock).toHaveBeenCalledTimes(0) + expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( + command, + config, + expect.objectContaining({ autoChoose: false }), + ) }) it('resolves id from index when allowed', async () => { @@ -75,19 +101,16 @@ describe('createChooseFn', () => { ...chooseOptionsDefaults(), allowIndex: true, } - chooseOptionsWithDefaultsMock.mockReturnValueOnce(opts) expect(await chooseSimpleType(command, 'simple-type-from-arg', opts)).toBe('selected-simple-type-id') - expect(stringTranslateToIdMock).toHaveBeenCalledTimes(1) - expect(stringTranslateToIdMock).toHaveBeenCalledWith( + expect(stringTranslateToIdMock).toHaveBeenCalledExactlyOnceWith( expect.objectContaining(config), 'simple-type-from-arg', expect.any(Function), ) - expect(selectFromListMock).toHaveBeenCalledTimes(1) - expect(selectFromListMock).toHaveBeenCalledWith( + expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( command, config, expect.objectContaining({ preselectedId: 'translated-simple-type-id' }), @@ -114,7 +137,6 @@ describe('createChooseFn', () => { ...chooseOptionsDefaults(), allowIndex: true, } - chooseOptionsWithDefaultsMock.mockReturnValueOnce(opts) expect(await chooseSimpleType(command, 'simple-type-from-arg', opts)).toBe('selected-simple-type-id') @@ -135,8 +157,7 @@ describe('createChooseFn', () => { expect(await listItems()).toBe(itemList) - expect(itemListMock).toHaveBeenCalledTimes(1) - expect(itemListMock).toHaveBeenCalledWith(command.client) + expect(itemListMock).toHaveBeenCalledExactlyOnceWith(command.client) }) }) }) diff --git a/src/lib/command/command-util.ts b/src/lib/command/command-util.ts index 85e70aeb..4d1803cd 100644 --- a/src/lib/command/command-util.ts +++ b/src/lib/command/command-util.ts @@ -97,27 +97,3 @@ export async function stringGetIdFromUser(fieldInfo: Sorting = { - allowIndex: boolean - verbose: boolean - useConfigDefault: boolean - listItems?: ListDataFunction - autoChoose?: boolean -} - -export const chooseOptionsDefaults = (): ChooseOptions => ({ - allowIndex: false, - verbose: false, - useConfigDefault: false, - autoChoose: false, -}) - -export const chooseOptionsWithDefaults = (options: Partial> | undefined): ChooseOptions => ({ - ...chooseOptionsDefaults(), - ...options, -}) diff --git a/src/lib/command/util/apps-util.ts b/src/lib/command/util/apps-util.ts index 11a129e8..77a6dffe 100644 --- a/src/lib/command/util/apps-util.ts +++ b/src/lib/command/util/apps-util.ts @@ -10,7 +10,7 @@ import { import { arrayDef, checkboxDef, stringDef } from '../../item-input/index.js' import { TableFieldDefinition, TableGenerator } from '../../table-generator.js' import { localhostOrHTTPSValidate } from '../../validate-util.js' -import { createChooseFn } from './util-util.js' +import { ChooseFunction, createChooseFn } from './util-util.js' export const isWebhookSmartApp = (app: AppResponse): boolean => !!app.webhookSmartApp @@ -45,7 +45,7 @@ export const tableFieldDefinitions: TableFieldDefinition[] = [ export const oauthTableFieldDefinitions: TableFieldDefinition[] = ['clientName', 'scope', 'redirectUris'] -export const chooseApp = createChooseFn( +export const chooseAppFn = (): ChooseFunction => createChooseFn( { itemName: 'app', primaryKeyName: 'appId', @@ -54,6 +54,8 @@ export const chooseApp = createChooseFn( (client: SmartThingsClient) => client.apps.list(), ) +export const chooseApp = chooseAppFn() + export const buildTableOutput = (tableGenerator: TableGenerator, appSettings: AppSettingsResponse): string => { if (!appSettings.settings || Object.keys(appSettings.settings).length === 0) { return 'No application settings.' diff --git a/src/lib/command/util/locations-util.ts b/src/lib/command/util/locations-util.ts index d3e2b4be..52f62e33 100644 --- a/src/lib/command/util/locations-util.ts +++ b/src/lib/command/util/locations-util.ts @@ -1,7 +1,7 @@ import { Location, SmartThingsClient } from '@smartthings/core-sdk' import { TableFieldDefinition } from '../../table-generator.js' -import { createChooseFn } from './util-util.js' +import { ChooseFunction, createChooseFn } from './util-util.js' export const tableFieldDefinitions: TableFieldDefinition[] = [ @@ -9,7 +9,7 @@ export const tableFieldDefinitions: TableFieldDefinition[] = [ 'latitude', 'longitude', 'regionRadius', 'temperatureScale', 'locale', ] -export const chooseLocation = createChooseFn( +export const chooseLocationFn = (): ChooseFunction => createChooseFn( { itemName: 'location', primaryKeyName: 'locationId', @@ -17,3 +17,5 @@ export const chooseLocation = createChooseFn( }, (client: SmartThingsClient) => client.locations.list(), ) + +export const chooseLocation = chooseLocationFn() diff --git a/src/lib/command/util/util-util.ts b/src/lib/command/util/util-util.ts index 66063492..34bc1cd5 100644 --- a/src/lib/command/util/util-util.ts +++ b/src/lib/command/util/util-util.ts @@ -1,10 +1,35 @@ import { SmartThingsClient } from '@smartthings/core-sdk' import { APICommand } from '../api-command.js' -import { ChooseOptions, chooseOptionsWithDefaults, stringTranslateToId } from '../command-util.js' +import { ListDataFunction } from '../basic-io.js' +import { stringTranslateToId } from '../command-util.js' import { SelectFromListConfig, SelectFromListFlags, selectFromList } from '../select.js' +/** + * Note that not all functions that use this interface support all options. Check the + * `chooseThing` (e.g. `chooseDevice`) method itself. + */ +export type ChooseOptions = { + allowIndex: boolean + verbose: boolean + useConfigDefault: boolean + listItems?: ListDataFunction + autoChoose?: boolean +} + +export const chooseOptionsDefaults = (): ChooseOptions => ({ + allowIndex: false, + verbose: false, + useConfigDefault: false, + autoChoose: false, +}) + +export const chooseOptionsWithDefaults = (options: Partial> | undefined): ChooseOptions => ({ + ...chooseOptionsDefaults(), + ...options, +}) + export type ChooseFunction = ( command: APICommand, locationFromArg?: string,