diff --git a/src/electron/flux/action/android-setup-actions.ts b/src/electron/flux/action/android-setup-actions.ts index 8f23ddcd7f4..46eb58a5907 100644 --- a/src/electron/flux/action/android-setup-actions.ts +++ b/src/electron/flux/action/android-setup-actions.ts @@ -6,7 +6,6 @@ import { Action } from 'common/flux/action'; // So, for example, the 'next' action may represent 'continue', 'str again', or 'start scanning' export class AndroidSetupActions { public readonly cancel = new Action(); - public readonly close = new Action(); public readonly next = new Action(); public readonly rescan = new Action(); public readonly saveAdbPath = new Action(); diff --git a/src/electron/platform/android/setup/android-setup-state-machine-factory.ts b/src/electron/platform/android/setup/android-setup-state-machine-factory.ts index e62f16bbda9..bffee7d06db 100644 --- a/src/electron/platform/android/setup/android-setup-state-machine-factory.ts +++ b/src/electron/platform/android/setup/android-setup-state-machine-factory.ts @@ -7,9 +7,10 @@ import { AndroidSetupStepTransitionCallback, } from 'electron/flux/types/android-setup-state-machine-types'; import { AndroidSetupStepId } from 'electron/platform/android/setup/android-setup-step-id'; -import { createAndroidSetupSteps } from 'electron/platform/android/setup/android-setup-steps-factory'; import { StateMachine } from 'electron/platform/android/setup/state-machine/state-machine'; +import { createStateMachineSteps } from 'electron/platform/android/setup/state-machine/state-machine-step-configs'; import { AndroidSetupStepDeps } from './android-setup-step-deps'; +import { allAndroidSetupStepConfigs } from './android-setup-steps-configs'; import { StateMachineSteps } from './state-machine/state-machine-steps'; type AndroidSetupStepsFactory = ( @@ -25,7 +26,7 @@ const stepsFactory = ( stepTransition: stateMachineStepTransition, }; - return createAndroidSetupSteps(allDeps); + return createStateMachineSteps(allDeps, allAndroidSetupStepConfigs); }; }; diff --git a/src/electron/platform/android/setup/android-setup-step-deps.ts b/src/electron/platform/android/setup/android-setup-step-deps.ts index 5a9bc6df8cb..70acc26dcd8 100644 --- a/src/electron/platform/android/setup/android-setup-step-deps.ts +++ b/src/electron/platform/android/setup/android-setup-step-deps.ts @@ -4,5 +4,6 @@ import { AndroidSetupStepId } from './android-setup-step-id'; export type AndroidSetupStepDeps = { + detectAdb: () => Promise; stepTransition: (nextStep: AndroidSetupStepId) => void; }; diff --git a/src/electron/platform/android/setup/android-setup-steps-configs.ts b/src/electron/platform/android/setup/android-setup-steps-configs.ts new file mode 100644 index 00000000000..57630eb845b --- /dev/null +++ b/src/electron/platform/android/setup/android-setup-steps-configs.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AndroidSetupActions } from 'electron/flux/action/android-setup-actions'; +import { AndroidSetupStepDeps } from 'electron/platform/android/setup/android-setup-step-deps'; +import { AndroidSetupStepId } from 'electron/platform/android/setup/android-setup-step-id'; +import { + StateMachineStepConfig, + StateMachineStepConfigs, +} from './state-machine/state-machine-step-configs'; +import { detectAdb } from './steps/detect.adb'; + +export type AndroidSetupStepConfig = StateMachineStepConfig< + AndroidSetupActions, + AndroidSetupStepDeps +>; + +type AndroidSetupStepConfigs = StateMachineStepConfigs< + AndroidSetupStepId, + AndroidSetupActions, + AndroidSetupStepDeps +>; + +export const allAndroidSetupStepConfigs: AndroidSetupStepConfigs = { + 'detect-adb': detectAdb, + 'prompt-locate-adb': null, + 'prompt-connect-to-device': null, + 'detect-devices': null, + 'prompt-choose-device': null, + 'detect-service': null, + 'prompt-install-service': null, + 'installing-service': null, + 'prompt-install-failed': null, + 'detect-permissions': null, + 'prompt-grant-permissions': null, + 'prompt-connected-start-testing': null, +}; diff --git a/src/electron/platform/android/setup/android-setup-steps-factory.ts b/src/electron/platform/android/setup/android-setup-steps-factory.ts deleted file mode 100644 index c22facb2043..00000000000 --- a/src/electron/platform/android/setup/android-setup-steps-factory.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { AndroidSetupActions } from 'electron/flux/action/android-setup-actions'; -import { AndroidSetupStepDeps } from 'electron/platform/android/setup/android-setup-step-deps'; -import { AndroidSetupStepId } from 'electron/platform/android/setup/android-setup-step-id'; -import { StateMachineStep } from 'electron/platform/android/setup/state-machine/state-machine-step'; -import { StateMachineSteps } from 'electron/platform/android/setup/state-machine/state-machine-steps'; -import { mapValues } from 'lodash'; - -type AndroidSetupStepConfig = (deps: AndroidSetupStepDeps) => StateMachineStep; - -type AndroidSetupStepConfigs = { - [stepId in AndroidSetupStepId]: AndroidSetupStepConfig; -}; - -const allAndroidSetupStepConfigs: AndroidSetupStepConfigs = { - 'detect-adb': null, - 'prompt-locate-adb': null, - 'prompt-connect-to-device': null, - 'detect-devices': null, - 'prompt-choose-device': null, - 'detect-service': null, - 'prompt-install-service': null, - 'installing-service': null, - 'prompt-install-failed': null, - 'detect-permissions': null, - 'prompt-grant-permissions': null, - 'prompt-connected-start-testing': null, -}; - -export const createAndroidSetupSteps = ( - deps: AndroidSetupStepDeps, -): StateMachineSteps => { - return mapValues(allAndroidSetupStepConfigs, config => config && config(deps)); -}; diff --git a/src/electron/platform/android/setup/state-machine/state-machine-step-configs.ts b/src/electron/platform/android/setup/state-machine/state-machine-step-configs.ts new file mode 100644 index 00000000000..ab97e0c3ce6 --- /dev/null +++ b/src/electron/platform/android/setup/state-machine/state-machine-step-configs.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { StateMachineSteps } from 'electron/platform/android/setup/state-machine/state-machine-steps'; +import { mapValues } from 'lodash'; +import { ActionBag } from './state-machine-action-callback'; +import { StateMachineStep } from './state-machine-step'; + +export type StateMachineStepConfig, DepsT> = ( + deps: DepsT, +) => StateMachineStep; + +export type StateMachineStepConfigs< + StepIdT extends string, + ActionT extends ActionBag, + DepsT +> = { + [stepId in StepIdT]: StateMachineStepConfig; +}; + +export const createStateMachineSteps = < + StepIdT extends string, + ActionT extends ActionBag, + DepsT +>( + deps: DepsT, + configs: StateMachineStepConfigs, +): StateMachineSteps => { + return mapValues(configs, config => config && config(deps)); +}; diff --git a/src/electron/platform/android/setup/state-machine/state-machine-step.ts b/src/electron/platform/android/setup/state-machine/state-machine-step.ts index 825a6ddd36d..528f6faa129 100644 --- a/src/electron/platform/android/setup/state-machine/state-machine-step.ts +++ b/src/electron/platform/android/setup/state-machine/state-machine-step.ts @@ -8,5 +8,5 @@ export type StateMachineStep> = { // eg, 'adb-location-confirmed': (newAdbLocation) => { /* behavior */ } [actionName in keyof ActionT]?: StateMachineActionCallback; }; - onEnter?: () => void; + onEnter?: () => Promise; }; diff --git a/src/electron/platform/android/setup/state-machine/state-machine.ts b/src/electron/platform/android/setup/state-machine/state-machine.ts index 99fb7a0aad5..e143c6ba7b5 100644 --- a/src/electron/platform/android/setup/state-machine/state-machine.ts +++ b/src/electron/platform/android/setup/state-machine/state-machine.ts @@ -46,12 +46,15 @@ export class StateMachine console.log(`UnExpected error in ${nextStep}.onEnter: ${error}`)); } + this.currentStep = nextStep; + this.stepTransitionCallback(nextStep); }; } diff --git a/src/electron/platform/android/setup/steps/detect.adb.ts b/src/electron/platform/android/setup/steps/detect.adb.ts new file mode 100644 index 00000000000..6905ea5e998 --- /dev/null +++ b/src/electron/platform/android/setup/steps/detect.adb.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AndroidSetupStepConfig } from 'electron/platform/android/setup/android-setup-steps-configs'; + +export const detectAdb: AndroidSetupStepConfig = deps => ({ + actions: {}, + onEnter: async () => { + const detected = await deps.detectAdb(); + deps.stepTransition(detected ? 'detect-devices' : 'prompt-locate-adb'); + }, +}); diff --git a/src/electron/views/renderer-initializer.ts b/src/electron/views/renderer-initializer.ts index e56611cef92..fb6db9837ad 100644 --- a/src/electron/views/renderer-initializer.ts +++ b/src/electron/views/renderer-initializer.ts @@ -74,6 +74,7 @@ import { createDeviceConfigFetcher } from 'electron/platform/android/device-conf import { createScanResultsFetcher } from 'electron/platform/android/fetch-scan-results'; import { ScanController } from 'electron/platform/android/scan-controller'; import { createAndroidSetupStateMachineFactory } from 'electron/platform/android/setup/android-setup-state-machine-factory'; +import { AndroidSetupStepDeps } from 'electron/platform/android/setup/android-setup-step-deps'; import { createDefaultBuilder } from 'electron/platform/android/unified-result-builder'; import { UnifiedSettingsProvider } from 'electron/settings/unified-settings-provider'; import { defaultAndroidSetupComponents } from 'electron/views/device-connect-view/components/android-setup/default-android-setup-components'; @@ -188,7 +189,7 @@ getPersistedData(indexedDBInstance, indexedDBDataKeysToFetch).then( const androidSetupStore = new AndroidSetupStore( androidSetupActions, - createAndroidSetupStateMachineFactory({}), + createAndroidSetupStateMachineFactory({} as AndroidSetupStepDeps), ); androidSetupStore.initialize(); diff --git a/src/tests/unit/tests/electron/flux/store/android-setup-store.test.ts b/src/tests/unit/tests/electron/flux/store/android-setup-store.test.ts index 1d899fa4561..0f88358ee38 100644 --- a/src/tests/unit/tests/electron/flux/store/android-setup-store.test.ts +++ b/src/tests/unit/tests/electron/flux/store/android-setup-store.test.ts @@ -48,7 +48,6 @@ describe('AndroidSetupStore', () => { store.initialize(); setupActions.cancel.invoke(); - setupActions.close.invoke(); setupActions.next.invoke(); setupActions.rescan.invoke(); setupActions.saveAdbPath.invoke(''); diff --git a/src/tests/unit/tests/electron/platform/android/setup/android-setup-steps-factory.test.ts b/src/tests/unit/tests/electron/platform/android/setup/android-setup-steps-factory.test.ts deleted file mode 100644 index 1e8cb847c58..00000000000 --- a/src/tests/unit/tests/electron/platform/android/setup/android-setup-steps-factory.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { AndroidSetupActions } from 'electron/flux/action/android-setup-actions'; -import { AndroidSetupStepDeps } from 'electron/platform/android/setup/android-setup-step-deps'; -import { AndroidSetupStepId } from 'electron/platform/android/setup/android-setup-step-id'; -import { createAndroidSetupSteps } from 'electron/platform/android/setup/android-setup-steps-factory'; -import { StateMachineSteps } from 'electron/platform/android/setup/state-machine/state-machine-steps'; -import { It, Mock, Times } from 'typemoq'; - -describe('android setup steps factory', () => { - it('returns expected steps', () => { - const stepTransitionMock = Mock.ofInstance((_: AndroidSetupStepId) => {}); - stepTransitionMock.setup(m => m(It.isAny())).verifiable(Times.never()); - - const deps: AndroidSetupStepDeps = { - stepTransition: stepTransitionMock.object, - }; - - // The following object should be filled out with the results of individual step factories - // called with the deps object created above. - // Values are allowed to be null while steps are under construction - const allAndroidSetupSteps: StateMachineSteps = { - 'detect-adb': null, - 'prompt-locate-adb': null, - 'prompt-connect-to-device': null, - 'detect-devices': null, - 'prompt-choose-device': null, - 'detect-service': null, - 'prompt-install-service': null, - 'installing-service': null, - 'prompt-install-failed': null, - 'detect-permissions': null, - 'prompt-grant-permissions': null, - 'prompt-connected-start-testing': null, - }; - - const steps = createAndroidSetupSteps(deps); - expect(steps).toStrictEqual(allAndroidSetupSteps); - - stepTransitionMock.verifyAll(); - }); -}); diff --git a/src/tests/unit/tests/electron/platform/android/setup/state-machine/state-machine-step-configs.test.ts b/src/tests/unit/tests/electron/platform/android/setup/state-machine/state-machine-step-configs.test.ts new file mode 100644 index 00000000000..2bef6d57981 --- /dev/null +++ b/src/tests/unit/tests/electron/platform/android/setup/state-machine/state-machine-step-configs.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Action } from 'common/flux/action'; +import { StateMachineStep } from 'electron/platform/android/setup/state-machine/state-machine-step'; +import { + createStateMachineSteps, + StateMachineStepConfigs, +} from 'electron/platform/android/setup/state-machine/state-machine-step-configs'; +import { StateMachineSteps } from 'electron/platform/android/setup/state-machine/state-machine-steps'; + +type MartiniStepId = 'gin' | 'vermouth' | 'olives'; + +class MartiniActions { + public shake = new Action(); + public stir = new Action(); +} + +type MartiniDeps = { + chillGlass: () => string; +}; + +type MartiniStep = StateMachineStep; + +describe('state machine step configs', () => { + const martiniDeps = {} as MartiniDeps; + + it('calls config functions with expected deps', () => { + const testDeps = deps => { + expect(deps).toBe(martiniDeps); + expect(deps).toEqual(martiniDeps); + return null; + }; + + const configs: StateMachineStepConfigs = { + gin: deps => testDeps(deps), + vermouth: deps => testDeps(deps), + olives: deps => testDeps(deps), + }; + + createStateMachineSteps(martiniDeps, configs); + }); + + it('handles null config functions gracefully', () => { + const expectedSteps: StateMachineSteps = { + gin: null, + vermouth: null, + olives: null, + }; + + const configs: StateMachineStepConfigs = { + gin: null, + vermouth: null, + olives: null, + }; + + let testSteps: StateMachineSteps; + const testFunc = () => (testSteps = createStateMachineSteps(martiniDeps, configs)); + expect(testFunc).not.toThrow(); + expect(testSteps).toEqual(expectedSteps); + }); + + it('returns expected object', () => { + const ginStep: MartiniStep = { + actions: { + shake: null, + }, + }; + + const vermouthStep: MartiniStep = { + actions: { + stir: null, + }, + }; + + const configs: StateMachineStepConfigs = { + gin: _ => ginStep, + vermouth: _ => vermouthStep, + olives: null, + }; + + const testSteps = createStateMachineSteps(martiniDeps, configs); + + expect(testSteps.gin).toBe(ginStep); + expect(testSteps.gin).toEqual(ginStep); + + expect(testSteps.vermouth).toBe(vermouthStep); + expect(testSteps.vermouth).toEqual(vermouthStep); + }); +}); diff --git a/src/tests/unit/tests/electron/platform/android/setup/state-machine/state-machine.test.ts b/src/tests/unit/tests/electron/platform/android/setup/state-machine/state-machine.test.ts index 633b126c113..50404575352 100644 --- a/src/tests/unit/tests/electron/platform/android/setup/state-machine/state-machine.test.ts +++ b/src/tests/unit/tests/electron/platform/android/setup/state-machine/state-machine.test.ts @@ -169,8 +169,11 @@ describe('Android setup state machine', () => { }); it('onEnter is invoked when transition happens', () => { - const onEnterMock = Mock.ofInstance(() => {}); - onEnterMock.setup(m => m()).verifiable(Times.once()); + const onEnterMock = Mock.ofInstance(async () => {}); + onEnterMock + .setup(m => m()) + .returns(_ => new Promise((resolve, reject) => resolve())) + .verifiable(Times.once()); const otherFactory = ( onStepTransition: MartiniStepTransition, @@ -194,7 +197,43 @@ describe('Android setup state machine', () => { 'gin', ); - expect(sm).toBeTruthy(); + expect(sm).toBeDefined(); + onEnterMock.verifyAll(); + stepTransitionCallbackMock.verifyAll(); + }); + + it('onEnter promise rejected has no side effects ', () => { + const onEnterMock = Mock.ofInstance(async () => {}); + onEnterMock + .setup(m => m()) + .returns( + _ => new Promise((resolve, reject) => reject(new Error('sample exception output'))), + ) + .verifiable(Times.once()); + + const otherFactory = ( + onStepTransition: MartiniStepTransition, + ): StateMachineSteps => { + return { + gin: { + actions: {}, + onEnter: onEnterMock.object, + }, + vermouth: null, + olives: null, + }; + }; + + const stepTransitionCallbackMock = Mock.ofInstance((_: MartiniStepId) => {}); + stepTransitionCallbackMock.setup(m => m('gin')).verifiable(Times.once()); + + const sm = new StateMachine( + otherFactory, + stepTransitionCallbackMock.object, + 'gin', + ); + + expect(sm).toBeDefined(); onEnterMock.verifyAll(); stepTransitionCallbackMock.verifyAll(); }); @@ -255,7 +294,7 @@ describe('Android setup state machine', () => { it('does not catch exceptions when calling onEnter', () => { const error = new Error('my error'); - const onEnterMock = Mock.ofInstance(() => {}); + const onEnterMock = Mock.ofInstance(async () => {}); onEnterMock .setup(m => m()) .throws(error) diff --git a/src/tests/unit/tests/electron/platform/android/setup/steps/actions-tester.ts b/src/tests/unit/tests/electron/platform/android/setup/steps/actions-tester.ts new file mode 100644 index 00000000000..c47ec772d61 --- /dev/null +++ b/src/tests/unit/tests/electron/platform/android/setup/steps/actions-tester.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AndroidSetupActions } from 'electron/flux/action/android-setup-actions'; +import { StateMachineStep } from 'electron/platform/android/setup/state-machine/state-machine-step'; + +export const checkExpectedActionsAreDefined = ( + step: StateMachineStep, + expectedActions?: (keyof AndroidSetupActions)[], +): void => { + expectedActions = expectedActions ?? []; + + const actionNames = Object.keys(new AndroidSetupActions()) as (keyof AndroidSetupActions)[]; + + for (const actionName of actionNames) { + if (expectedActions.includes(actionName)) { + expect(step.actions[actionName]).toBeDefined(); + } else { + expect(step.actions[actionName]).not.toBeDefined(); + } + } +}; diff --git a/src/tests/unit/tests/electron/platform/android/setup/steps/detect-adb.test.ts b/src/tests/unit/tests/electron/platform/android/setup/steps/detect-adb.test.ts new file mode 100644 index 00000000000..4ef4b986ca9 --- /dev/null +++ b/src/tests/unit/tests/electron/platform/android/setup/steps/detect-adb.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { AndroidSetupStepDeps } from 'electron/platform/android/setup/android-setup-step-deps'; +import { detectAdb } from 'electron/platform/android/setup/steps/detect.adb'; +import { checkExpectedActionsAreDefined } from 'tests/unit/tests/electron/platform/android/setup/steps/actions-tester'; +import { Mock, MockBehavior, Times } from 'typemoq'; + +describe('Android setup step: detectAdb', () => { + it('has expected properties', () => { + const deps = {} as AndroidSetupStepDeps; + const step = detectAdb(deps); + checkExpectedActionsAreDefined(step, []); + expect(step.onEnter).toBeDefined(); + }); + + it('onEnter transitions to detect-devices as expected', async () => { + const p = new Promise(resolve => resolve(true)); + + const depsMock = Mock.ofType(undefined, MockBehavior.Strict); + depsMock + .setup(m => m.detectAdb()) + .returns(_ => p) + .verifiable(Times.once()); + + depsMock.setup(m => m.stepTransition('detect-devices')).verifiable(Times.once()); + + const step = detectAdb(depsMock.object); + await step.onEnter(); + + depsMock.verifyAll(); + }); + + it('onEnter transitions to prompt-locate-adb as expected', async () => { + const p = new Promise(resolve => resolve(false)); + + const depsMock = Mock.ofType(undefined, MockBehavior.Strict); + depsMock + .setup(m => m.detectAdb()) + .returns(_ => p) + .verifiable(Times.once()); + + depsMock.setup(m => m.stepTransition('prompt-locate-adb')).verifiable(Times.once()); + + const step = detectAdb(depsMock.object); + await step.onEnter(); + + depsMock.verifyAll(); + }); +});