diff --git a/src/actions/license.ts b/src/actions/license.ts index 3ab1a567..5a593a20 100644 --- a/src/actions/license.ts +++ b/src/actions/license.ts @@ -6,6 +6,7 @@ import { LicenseInfo, LicenseList } from '../reducers/license'; export enum LicenseActionType { DidFetchList = 'license.action.didFetchList', + DidFailToFetchList = 'license.action.didFailToFetchList', Select = 'license.action.select', } @@ -17,6 +18,14 @@ export function didFetchList(list: LicenseList): LicenseDidFetchListAction { return { type: LicenseActionType.DidFetchList, list }; } +export type LicenseDidFailToFetchListAction = Action & { + reason: Response; +}; + +export function didFailToFetchList(reason: Response): LicenseDidFailToFetchListAction { + return { type: LicenseActionType.DidFailToFetchList, reason }; +} + export type LicenseSelectAction = Action & { info: LicenseInfo; }; @@ -25,4 +34,7 @@ export function select(info: LicenseInfo): LicenseSelectAction { return { type: LicenseActionType.Select, info }; } -export type LicenseAction = LicenseDidFetchListAction | LicenseSelectAction; +export type LicenseAction = + | LicenseDidFetchListAction + | LicenseDidFailToFetchListAction + | LicenseSelectAction; diff --git a/src/sagas/error-log.test.ts b/src/sagas/error-log.test.ts index 15478f98..b0bd8f25 100644 --- a/src/sagas/error-log.test.ts +++ b/src/sagas/error-log.test.ts @@ -7,6 +7,7 @@ import { didFailToConnect as bleDidFailToConnect, } from '../actions/ble'; import { didFailToWrite } from '../actions/ble-uart'; +import { didFailToFetchList } from '../actions/license'; import { BootloaderConnectionFailureReason, didError, @@ -68,3 +69,17 @@ test('bootloaderDidError', async () => { await saga.end(); }); + +test('licenseDidFailToFetch', async () => { + const saga = new AsyncSaga(errorLog); + + console.error = jest.fn(); + saga.put( + didFailToFetchList( + new Response(undefined, { status: 404, statusText: 'not found' }), + ), + ); + expect(console.error).toHaveBeenCalledTimes(1); + + await saga.end(); +}); diff --git a/src/sagas/error-log.ts b/src/sagas/error-log.ts index 7457c029..d7e96dbe 100644 --- a/src/sagas/error-log.ts +++ b/src/sagas/error-log.ts @@ -8,6 +8,7 @@ import { BleDeviceFailToConnectReasonType, } from '../actions/ble'; import { BleUartActionType, BleUartDidFailToWriteAction } from '../actions/ble-uart'; +import { LicenseActionType, LicenseDidFailToFetchListAction } from '../actions/license'; import { BootloaderConnectionActionType, BootloaderConnectionDidErrorAction, @@ -39,6 +40,10 @@ function bootloaderDidError(action: BootloaderConnectionDidErrorAction): void { console.error(action.err); } +function licenseDidFailToFetch(action: LicenseDidFailToFetchListAction): void { + console.error(`Failed to fetch licenses: ${action.reason.statusText}`); +} + export default function* (): Generator { yield takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect); yield takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite); @@ -47,4 +52,5 @@ export default function* (): Generator { bootloaderDidFailToConnect, ); yield takeEvery(BootloaderConnectionActionType.DidError, bootloaderDidError); + yield takeEvery(LicenseActionType.DidFailToFetchList, licenseDidFailToFetch); } diff --git a/src/sagas/license.test.ts b/src/sagas/license.test.ts new file mode 100644 index 00000000..609072f3 --- /dev/null +++ b/src/sagas/license.test.ts @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020-2021 The Pybricks Authors + +// Tests for license sagas. + +import { AsyncSaga, delay } from '../../test'; +import { openLicenseDialog } from '../actions/app'; +import { didFailToFetchList, didFetchList } from '../actions/license'; +import { LicenseList, LicenseState } from '../reducers/license'; +import license from './license'; + +afterAll(() => { + jest.restoreAllMocks(); +}); + +describe('fetchLicenses', () => { + test('first call', async () => { + const testLicenseList: LicenseList = []; + const saga = new AsyncSaga(license); + + jest.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify(testLicenseList)), + ); + + // initially, license list starts as null, so fetch is called to get + // the list + saga.setState({ license: { list: null } as LicenseState }); + saga.put(openLicenseDialog()); + + const action = await saga.take(); + expect(action).toEqual(didFetchList(testLicenseList)); + + await saga.end(); + }); + test('second call', async () => { + const testLicenseList: LicenseList = []; + const saga = new AsyncSaga(license); + + jest.spyOn(globalThis, 'fetch').mockRejectedValue( + 'fetch () should not have been called', + ); + + // after we have the list, we don't fetch it again since it will + // always be the same list + saga.setState({ license: { list: testLicenseList } as LicenseState }); + saga.put(openLicenseDialog()); + + // have to yield to be sure fetch call would have taken place on error + await delay(0); + + await saga.end(); + }); + test('failed fetch', async () => { + const failResponse = new Response(undefined, { status: 404 }); + const saga = new AsyncSaga(license); + + jest.spyOn(globalThis, 'fetch').mockResolvedValue(failResponse); + + saga.setState({ license: { list: null } as LicenseState }); + saga.put(openLicenseDialog()); + + const action = await saga.take(); + expect(action).toEqual(didFailToFetchList(failResponse)); + + await saga.end(); + }); +}); diff --git a/src/sagas/license.ts b/src/sagas/license.ts index ff574f72..0bd1eac0 100644 --- a/src/sagas/license.ts +++ b/src/sagas/license.ts @@ -3,7 +3,7 @@ import { call, put, select, takeEvery } from 'redux-saga/effects'; import { AppActionType } from '../actions/app'; -import { didFetchList } from '../actions/license'; +import { didFailToFetchList, didFetchList } from '../actions/license'; import { RootState } from '../reducers'; import { LicenseList } from '../reducers/license'; @@ -19,8 +19,7 @@ function* fetchLicenses(): Generator { const response = (yield call(() => fetch('static/oss-licenses.json'))) as Response; if (!response.ok || response.body === null) { - // TODO: dispatch an action to notify user - console.error('failed to fetch oss-licenses.json', response.statusText); + yield put(didFailToFetchList(response)); return; }