Skip to content

Commit

Permalink
feat(app): Add GET /modules to API client (#1837)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous authored Jul 9, 2018
1 parent 464ed7f commit da88936
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 1 deletion.
92 changes: 92 additions & 0 deletions app/src/http-api-client/__tests__/modules.test.js
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})
})
})
})
118 changes: 118 additions & 0 deletions app/src/http-api-client/__tests__/reducer.test.js
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')
}
}
})
})
})
6 changes: 6 additions & 0 deletions app/src/http-api-client/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export type ClearApiResponseAction<Path: string> = {|
|}
|}

export type ApiAction<Path: string, Request: ?{}, Response: {}> =
| ApiRequestAction<Path, Request>
| ApiSuccessAction<Path, Response>
| ApiFailureAction<Path>
| ClearApiResponseAction<Path>

export function apiRequest<Path: string, Body: ?{}> (
robot: BaseRobot,
path: Path,
Expand Down
9 changes: 8 additions & 1 deletion app/src/http-api-client/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// @flow
// robot HTTP API client module
import {combineReducers} from 'redux'
import apiReducer from './reducer'
import {calibrationReducer, type CalibrationAction} from './calibration'
import {healthReducer, type HealthAction} from './health'
import type {ModulesAction} from './modules'
import {motorsReducer, type MotorsAction} from './motors'
import {pipettesReducer, type PipettesAction} from './pipettes'
import {robotReducer, type RobotAction} from './robot'
Expand All @@ -18,7 +20,9 @@ export const reducer = combineReducers({
robot: robotReducer,
server: serverReducer,
settings: settingsReducer,
wifi: wifiReducer
wifi: wifiReducer,
// TODO(mc, 2018-07-09): api subreducer will become the sole reducer
api: apiReducer
})

export * from './types'
Expand Down Expand Up @@ -79,6 +83,7 @@ export type State = $Call<typeof reducer>
export type Action =
| CalibrationAction
| HealthAction
| ModulesAction
| MotorsAction
| PipettesAction
| RobotAction
Expand All @@ -98,6 +103,8 @@ export {
makeGetRobotHealth
} from './health'

export * from './modules'

export {
disengagePipetteMotors
} from './motors'
Expand Down
57 changes: 57 additions & 0 deletions app/src/http-api-client/modules.js
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
}
75 changes: 75 additions & 0 deletions app/src/http-api-client/reducer.js
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}
}

0 comments on commit da88936

Please sign in to comment.