-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(app): Add GET /modules to API client (#1837)
- Loading branch information
Showing
6 changed files
with
356 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// http api /modules tests | ||
import configureMockStore from 'redux-mock-store' | ||
import thunk from 'redux-thunk' | ||
|
||
import client from '../client' | ||
import {fetchModules, makeGetRobotModules} from '..' | ||
|
||
jest.mock('../client') | ||
|
||
const middlewares = [thunk] | ||
const mockStore = configureMockStore(middlewares) | ||
|
||
const NAME = 'opentrons-dev' | ||
|
||
const modules = [ | ||
{ | ||
model: 'model', | ||
serial: 'serial', | ||
fwVersion: 'fwVersion', | ||
status: 'status', | ||
displayName: 'displayName' | ||
} | ||
] | ||
|
||
describe('/modules', () => { | ||
let robot | ||
let state | ||
let store | ||
|
||
beforeEach(() => { | ||
client.__clearMock() | ||
|
||
robot = {name: NAME, ip: '1.2.3.4', port: '1234'} | ||
state = {api: {api: {}}} | ||
store = mockStore(state) | ||
}) | ||
|
||
describe('fetchModules action creator', () => { | ||
const path = 'modules' | ||
const response = {modules} | ||
|
||
test('calls GET /modules', () => { | ||
client.__setMockResponse(response) | ||
|
||
return store.dispatch(fetchModules(robot)) | ||
.then(() => | ||
expect(client).toHaveBeenCalledWith(robot, 'GET', path)) | ||
}) | ||
|
||
test('dispatches api:REQUEST and api:SUCCESS', () => { | ||
const request = null | ||
const expectedActions = [ | ||
{type: 'api:REQUEST', payload: {robot, request, path}}, | ||
{type: 'api:SUCCESS', payload: {robot, response, path}} | ||
] | ||
|
||
client.__setMockResponse(response) | ||
|
||
return store.dispatch(fetchModules(robot)) | ||
.then(() => expect(store.getActions()).toEqual(expectedActions)) | ||
}) | ||
|
||
test('dispatches api:REQUEST and api:FAILURE', () => { | ||
const request = null | ||
const error = {name: 'ResponseError', status: 500, message: ''} | ||
const expectedActions = [ | ||
{type: 'api:REQUEST', payload: {robot, request, path}}, | ||
{type: 'api:FAILURE', payload: {robot, error, path}} | ||
] | ||
|
||
client.__setMockError(error) | ||
|
||
return store.dispatch(fetchModules(robot)) | ||
.then(() => expect(store.getActions()).toEqual(expectedActions)) | ||
}) | ||
}) | ||
|
||
describe('selectors', () => { | ||
beforeEach(() => { | ||
state.api.api[NAME] = { | ||
modules: {inProgress: true} | ||
} | ||
}) | ||
|
||
test('makeGetRobotModules', () => { | ||
const getModules = makeGetRobotModules() | ||
|
||
expect(getModules(state, robot)).toEqual(state.api.api[NAME].modules) | ||
expect(getModules(state, {name: 'foo'})).toEqual({inProgress: false}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// tests for generic api reducer | ||
import apiReducer from '../reducer' | ||
|
||
describe('apiReducer', () => { | ||
test('handles api:REQUEST', () => { | ||
const emptyState = {} | ||
const oldRequestState = { | ||
name: { | ||
otherPath: {inProgress: false}, | ||
path: { | ||
inProgress: false, | ||
request: {}, | ||
response: {baz: 'qux'}, | ||
error: new Error('AH') | ||
} | ||
} | ||
} | ||
|
||
const action = { | ||
type: 'api:REQUEST', | ||
payload: {robot: {name: 'name'}, path: 'path', request: {foo: 'bar'}} | ||
} | ||
|
||
expect(apiReducer(emptyState, action)).toEqual({ | ||
name: { | ||
path: {inProgress: true, request: {foo: 'bar'}, error: null} | ||
} | ||
}) | ||
|
||
expect(apiReducer(oldRequestState, action)).toEqual({ | ||
name: { | ||
otherPath: {inProgress: false}, | ||
path: { | ||
inProgress: true, | ||
request: {foo: 'bar'}, | ||
response: {baz: 'qux'}, | ||
error: null | ||
} | ||
} | ||
}) | ||
}) | ||
|
||
test('handles api:SUCCESS', () => { | ||
const emptyState = {} | ||
const oldRequestState = { | ||
name: { | ||
otherPath: {inProgress: false}, | ||
path: { | ||
inProgress: true, | ||
request: {foo: 'bar'}, | ||
response: {fizz: 'buzz'}, | ||
error: new Error('AH') | ||
} | ||
} | ||
} | ||
|
||
const action = { | ||
type: 'api:SUCCESS', | ||
payload: {robot: {name: 'name'}, path: 'path', response: {baz: 'qux'}} | ||
} | ||
|
||
expect(apiReducer(emptyState, action)).toEqual({ | ||
name: { | ||
path: {inProgress: false, response: {baz: 'qux'}, error: null} | ||
} | ||
}) | ||
|
||
expect(apiReducer(oldRequestState, action)).toEqual({ | ||
name: { | ||
otherPath: {inProgress: false}, | ||
path: { | ||
inProgress: false, | ||
request: {foo: 'bar'}, | ||
response: {baz: 'qux'}, | ||
error: null | ||
} | ||
} | ||
}) | ||
}) | ||
|
||
test('handles api:FAILURE', () => { | ||
const emptyState = {} | ||
const oldRequestState = { | ||
name: { | ||
otherPath: {inProgress: false}, | ||
path: { | ||
inProgress: true, | ||
request: {foo: 'bar'}, | ||
response: {baz: 'qux'}, | ||
error: null | ||
} | ||
} | ||
} | ||
|
||
const action = { | ||
type: 'api:FAILURE', | ||
payload: {robot: {name: 'name'}, path: 'path', error: new Error('AH')} | ||
} | ||
|
||
expect(apiReducer(emptyState, action)).toEqual({ | ||
name: { | ||
path: {inProgress: false, error: new Error('AH')} | ||
} | ||
}) | ||
|
||
expect(apiReducer(oldRequestState, action)).toEqual({ | ||
name: { | ||
otherPath: {inProgress: false}, | ||
path: { | ||
inProgress: false, | ||
request: {foo: 'bar'}, | ||
response: {baz: 'qux'}, | ||
error: new Error('AH') | ||
} | ||
} | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// @flow | ||
// API client for modules (the robot kind) | ||
import {createSelector, type Selector} from 'reselect' | ||
|
||
import type {State, ThunkPromiseAction} from '../types' | ||
import type {BaseRobot, RobotService} from '../robot' | ||
import type {ApiCall, ApiRequestError} from './types' | ||
import type {ApiAction} from './actions' | ||
|
||
import {apiRequest, apiSuccess, apiFailure} from './actions' | ||
import {getRobotApiState} from './reducer' | ||
import client from './client' | ||
|
||
export type Module = { | ||
model: string, | ||
serial: string, | ||
fwVersion: string, | ||
status: string, | ||
displayName: string, | ||
} | ||
|
||
type FetchModulesResponse = { | ||
modules: Array<Module>, | ||
} | ||
|
||
type FetchModulesCall = ApiCall<null, FetchModulesResponse> | ||
|
||
export type ModulesAction = | ||
| ApiAction<'modules', null, FetchModulesResponse> | ||
|
||
export type ModulesState = { | ||
modules?: FetchModulesCall | ||
} | ||
|
||
const MODULES: 'modules' = 'modules' | ||
|
||
export function fetchModules (robot: RobotService): ThunkPromiseAction { | ||
return (dispatch) => { | ||
dispatch(apiRequest(robot, MODULES, null)) | ||
|
||
return client(robot, 'GET', MODULES) | ||
.then( | ||
(resp: FetchModulesResponse) => apiSuccess(robot, MODULES, resp), | ||
(err: ApiRequestError) => apiFailure(robot, MODULES, err) | ||
) | ||
.then(dispatch) | ||
} | ||
} | ||
|
||
export function makeGetRobotModules () { | ||
const selector: Selector<State, BaseRobot, FetchModulesCall> = createSelector( | ||
getRobotApiState, | ||
(state) => state[MODULES] || {inProgress: false} | ||
) | ||
|
||
return selector | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// @flow | ||
// generic api reducer | ||
|
||
import type {State, Action} from '../types' | ||
import type {BaseRobot} from '../robot' | ||
import type {ModulesState} from './modules' | ||
|
||
type RobotApiState = | ||
& ModulesState | ||
|
||
type ApiState = {[name: string]: ?RobotApiState} | ||
|
||
export default function apiReducer ( | ||
state: ApiState = {}, | ||
action: Action | ||
): ApiState { | ||
switch (action.type) { | ||
case 'api:REQUEST': { | ||
const {request} = action.payload | ||
const {name, path, stateByName, stateByPath} = getUpdateInfo(state, action) | ||
|
||
return { | ||
...state, | ||
[name]: { | ||
...stateByName, | ||
[path]: {...stateByPath, request, inProgress: true, error: null} | ||
} | ||
} | ||
} | ||
|
||
case 'api:SUCCESS': { | ||
const {response} = action.payload | ||
const {name, path, stateByName, stateByPath} = getUpdateInfo(state, action) | ||
|
||
return { | ||
...state, | ||
[name]: { | ||
...stateByName, | ||
[path]: {...stateByPath, response, inProgress: false, error: null} | ||
} | ||
} | ||
} | ||
|
||
case 'api:FAILURE': { | ||
const {error} = action.payload | ||
const {name, path, stateByName, stateByPath} = getUpdateInfo(state, action) | ||
|
||
return { | ||
...state, | ||
[name]: { | ||
...stateByName, | ||
[path]: {...stateByPath, error, inProgress: false} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return state | ||
} | ||
|
||
export function getRobotApiState ( | ||
state: State, | ||
props: BaseRobot | ||
): RobotApiState { | ||
return state.api.api[props.name] || {} | ||
} | ||
|
||
function getUpdateInfo (state: ApiState, action: *): * { | ||
const {path, robot: {name}} = action.payload | ||
const stateByName = state[name] || {} | ||
// $FlowFixMe: type RobotApiState properly | ||
const stateByPath = stateByName[path] || {} | ||
|
||
return {name, path, stateByName, stateByPath} | ||
} |