diff --git a/src/hub/reducers.test.ts b/src/hub/reducers.test.ts new file mode 100644 index 00000000..8c403ae9 --- /dev/null +++ b/src/hub/reducers.test.ts @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { Action } from '../actions'; +import { statusReportEvent } from '../ble-pybricks-service/actions'; +import { Status, statusToFlag } from '../ble-pybricks-service/protocol'; +import { didDisconnect } from '../ble/actions'; +import { + didFailToFinishDownload, + didFinishDownload, + didProgressDownload, + didStartDownload, +} from './actions'; +import reducers, { HubRuntimeState } from './reducers'; + +type State = ReturnType; + +test('initial state', () => { + expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(` + Object { + "downloadProgress": null, + "runtime": "hub.runtime.disconnected", + } + `); +}); + +describe('runtime', () => { + test.each(Object.values(HubRuntimeState))('didDisconnect', (startingState) => { + // all states are overridden by disconnect + expect( + reducers({ runtime: startingState } as State, didDisconnect()).runtime, + ).toBe(HubRuntimeState.Disconnected); + }); + + test('didStartDownload', () => { + // download ignored if disconnected + expect( + reducers( + { runtime: HubRuntimeState.Disconnected } as State, + didStartDownload(), + ).runtime, + ).toBe(HubRuntimeState.Disconnected); + expect( + reducers({ runtime: HubRuntimeState.Idle } as State, didStartDownload()) + .runtime, + ).toBe(HubRuntimeState.Loading); + }); + + test('didProgressDownload', () => { + // download ignored if disconnected + expect( + reducers( + { runtime: HubRuntimeState.Disconnected } as State, + didProgressDownload(0), + ).runtime, + ).toBe(HubRuntimeState.Disconnected); + + // this shouldn't have any effect + expect( + reducers( + { runtime: HubRuntimeState.Loading } as State, + didProgressDownload(0), + ).runtime, + ).toBe(HubRuntimeState.Loading); + }); + + test('didFinishDownload', () => { + // download ignored if disconnected + expect( + reducers( + { runtime: HubRuntimeState.Disconnected } as State, + didFinishDownload(), + ).runtime, + ).toBe(HubRuntimeState.Disconnected); + + // normal operation + expect( + reducers({ runtime: HubRuntimeState.Loading } as State, didFinishDownload()) + .runtime, + ).toBe(HubRuntimeState.Loaded); + }); + + test('didFinishDownload', () => { + // download ignored if disconnected + expect( + reducers( + { runtime: HubRuntimeState.Disconnected } as State, + didFailToFinishDownload(), + ).runtime, + ).toBe(HubRuntimeState.Disconnected); + + // normal operation for error path + expect( + reducers( + { runtime: HubRuntimeState.Loading } as State, + didFailToFinishDownload(), + ).runtime, + ).toBe(HubRuntimeState.Idle); + }); + + test('statusReportEvent', () => { + // don't ever expect this to happen in practice since we can't receive + // updates while disconnected + expect( + reducers( + { runtime: HubRuntimeState.Disconnected } as State, + statusReportEvent(statusToFlag(Status.UserProgramRunning)), + ).runtime, + ).toBe(HubRuntimeState.Disconnected); + + // status update ignored while download not finished + expect( + reducers( + { runtime: HubRuntimeState.Loading } as State, + statusReportEvent(statusToFlag(Status.UserProgramRunning)), + ).runtime, + ).toBe(HubRuntimeState.Loading); + + // normal operation - user program started + expect( + reducers( + { runtime: HubRuntimeState.Loaded } as State, + statusReportEvent(statusToFlag(Status.UserProgramRunning)), + ).runtime, + ).toBe(HubRuntimeState.Running); + + // really short program run finished before receiving download finished + expect( + reducers({ runtime: HubRuntimeState.Loaded } as State, statusReportEvent(0)) + .runtime, + ).toBe(HubRuntimeState.Idle); + + // normal operation - user program stopped + expect( + reducers( + { runtime: HubRuntimeState.Running } as State, + statusReportEvent(0), + ).runtime, + ).toBe(HubRuntimeState.Idle); + }); +}); diff --git a/src/hub/reducers.ts b/src/hub/reducers.ts index 822456c8..5f87654b 100644 --- a/src/hub/reducers.ts +++ b/src/hub/reducers.ts @@ -46,21 +46,38 @@ const runtime: Reducer = ( case BleDeviceActionType.DidDisconnect: return HubRuntimeState.Disconnected; case HubActionType.DidStartDownload: + // disconnected overrides download + if (state === HubRuntimeState.Disconnected) { + return state; + } return HubRuntimeState.Loading; case HubActionType.DidFinishDownload: + // disconnected overrides download + if (state === HubRuntimeState.Disconnected) { + return state; + } return HubRuntimeState.Loaded; case HubActionType.DidFailToFinishDownload: + // disconnected overrides download + if (state === HubRuntimeState.Disconnected) { + return state; + } return HubRuntimeState.Idle; case BlePybricksServiceEventActionType.StatusReport: // The loading state is determined solely by the IDE, so we can't // let the hub status interfere with it. - if (state !== HubRuntimeState.Loading) { - if (action.statusFlags & statusToFlag(Status.UserProgramRunning)) { - return HubRuntimeState.Running; - } - return HubRuntimeState.Idle; + if ( + state === HubRuntimeState.Disconnected || + state === HubRuntimeState.Loading + ) { + return state; } - return state; + + if (action.statusFlags & statusToFlag(Status.UserProgramRunning)) { + return HubRuntimeState.Running; + } + + return HubRuntimeState.Idle; default: return state; }