Add packages/client/src/crud

This commit is contained in:
Jerko Steiner 2019-04-01 12:56:31 +08:00
parent eed79f35f2
commit 73be64a900
5 changed files with 221 additions and 69 deletions

View File

@ -1,7 +1,9 @@
import React from 'react'
import {AnyAction} from 'redux'
import {CRUDActions, CRUDReducer} from './'
import {CRUDActions, CRUDReducer, ICRUDMethod} from './'
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
import {IMethod} from '@rondo/common'
import {IPendingAction} from '../actions'
describe('CRUD', () => {
@ -27,7 +29,6 @@ describe('CRUD', () => {
'/one/:oneId/two/:twoId': {
get: {
params: ITwoSpecificParams
body: ITwoCreateBody
response: ITwo
}
put: {
@ -37,7 +38,7 @@ describe('CRUD', () => {
}
delete: {
params: ITwoSpecificParams
response: {id: number}
response: {id: number} // TODO return ITwoSpecificParams
}
}
'/one/:oneId/two': {
@ -60,7 +61,8 @@ describe('CRUD', () => {
specificRoute: '/one/:oneId/two/:twoId',
actionName: 'TEST',
})
const Crud = new CRUDReducer<ITwo>('TEST').reduce
const crudReducer = new CRUDReducer<ITwo>('TEST')
const Crud = crudReducer.reduce
const test = new TestUtils()
const reducer = test.combineReducers({
@ -73,6 +75,8 @@ describe('CRUD', () => {
return test.createStore({reducer})()
}
type Store = ReturnType<typeof getStore>
afterEach(() => {
http.mockClear()
})
@ -83,70 +87,98 @@ describe('CRUD', () => {
})
})
describe('GET_MANY', () => {
function getAction(store: ReturnType<typeof getStore>) {
const action = store.dispatch(actions.getMany({
query: {},
params: {oneId: 1},
}))
function expectActions(actionTypes: string[]) {
const state = store.getState()
expect(state.Crud.status.getMany.isLoading).toBe(true)
expect(state.Logger).toEqual([
jasmine.any(String),
'TEST_GET_MANY_PENDING',
])
// first action is redux initializer
expect(state.Logger.slice(1)).toEqual(actionTypes)
}
let store: Store
beforeEach(() => {
store = getStore()
})
function dispatch(
method: ICRUDMethod,
action: IPendingAction<unknown, string>,
) {
store.dispatch(action)
expect(store.getState().Crud.status[method].isLoading).toBe(true)
expectActions([action.type])
return action
}
describe('TEST_GET_MANY_RESOLVED', () => {
function getUrl(method: ICRUDMethod) {
return method === 'post' || method === 'getMany'
? '/one/1/two'
: '/one/1/two/2'
}
function getHTTPMethod(method: ICRUDMethod): IMethod {
return method === 'getMany' ? 'get' : method
}
describe('Promise rejections', () => {
const testCases: Array<{
method: ICRUDMethod
params: any
}> = [{
method: 'get',
params: {
params: {oneId: 1, twoId: 2},
},
}, {
method: 'getMany',
params: {
params: {oneId: 1},
},
}, {
method: 'post',
params: {
body: {name: 'test'},
params: {oneId: 1, twoId: 2},
},
}, {
method: 'put',
params: {
body: {name: 'test'},
params: {oneId: 1, twoId: 2},
},
}, {
method: 'delete',
params: {
body: {},
params: {oneId: 1, twoId: 2},
},
}]
testCases.forEach(testCase => {
const {method} = testCase
describe(method, () => {
beforeEach(() => {
http.mockAdd({
method: 'get',
url: '/one/1/two',
params: {},
}, [{id: 2, name: 'bla'}])
})
it('updates state', async () => {
const store = getStore()
const action = getAction(store)
await action.payload
const state = store.getState()
expect(state.Crud.status.getMany.isLoading).toBe(false)
expect(state.Crud.ids).toEqual([2])
expect(state.Crud.byId[2]).toEqual({id: 2, name: 'bla'})
expect(state.Logger).toEqual([
jasmine.any(String),
'TEST_GET_MANY_PENDING',
'TEST_GET_MANY_RESOLVED',
])
})
})
describe('TEST_GET_MANY_REJECTED', () => {
beforeEach(() => {
http.mockAdd({
method: 'get',
url: '/one/1/two',
params: {},
url: getUrl(method),
method: getHTTPMethod(method),
data: method === 'put' || method === 'post' || method === 'delete'
? testCase.params.body
: undefined,
}, {error: 'Test Error'}, 400)
})
it('updates state', async () => {
const store = getStore()
const action = getAction(store)
it(`updates status on error: ${method}`, async () => {
const action = actions[method](testCase.params)
dispatch(testCase.method, action)
await getError(action.payload)
const state = store.getState()
expect(state.Crud.status.getMany.isLoading).toBe(false)
// TODO use error from response
expect(state.Crud.status.getMany.error).toBe('HTTP Status: 400')
expect(state.Crud.ids).toEqual([])
expect(state.Crud.byId).toEqual({})
expect(state.Logger).toEqual([
jasmine.any(String),
'TEST_GET_MANY_PENDING',
'TEST_GET_MANY_REJECTED',
expect(state.Crud.ids).toEqual([])
expect(state.Crud.status[method].isLoading).toBe(false)
// TODO use error from response
expect(state.Crud.status[method].error).toEqual('HTTP Status: 400')
expectActions([
action.type,
action.type.replace(/_PENDING$/, '_REJECTED'),
])
})
})
@ -154,3 +186,115 @@ describe('CRUD', () => {
})
})
describe('Resolved promises', () => {
const entity = {id: 100, name: 'test'}
const testCases: Array<{
method: ICRUDMethod
params: any
body?: any
response: any
}> = [{
method: 'getMany',
params: {oneId: 1, twoId: 2},
response: [entity],
}, {
method: 'get',
params: {oneId: 1, twoId: 2},
response: entity,
}, {
method: 'post',
params: {oneId: 1},
body: {name: entity.name},
response: entity,
}, {
method: 'put',
params: {oneId: 1, twoId: 2},
body: {name: entity.name},
response: entity,
}, {
method: 'delete',
params: {oneId: 1, twoId: 2},
response: {id: entity.id},
}]
testCases.forEach(testCase => {
const {method} = testCase
describe(method, () => {
beforeEach(() => {
http.mockAdd({
url: getUrl(method),
method: getHTTPMethod(method),
data: testCase.body,
}, testCase.response)
})
afterEach(() => {
http.mockClear()
})
it('updates state', async () => {
const action = dispatch(testCase.method, actions[method]({
query: undefined,
params: testCase.params,
body: testCase.body,
}))
await action.payload
const state = store.getState()
expect(state.Crud.status.getMany.isLoading).toBe(false)
if (method === 'delete') {
expect(state.Crud.ids).toEqual([])
expect(state.Crud.byId[entity.id]).toBe(undefined)
} else {
if (method !== 'put') {
expect(state.Crud.ids).toEqual([entity.id])
}
expect(state.Crud.byId[entity.id]).toEqual(entity)
}
})
})
})
describe('POST then DELETE', () => {
const postTestCase = testCases.find(t => t.method === 'post')!
const deleteTestCase = testCases.find(t => t.method === 'delete')!
beforeEach(() => {
http.mockAdd({
url: getUrl(postTestCase.method),
method: getHTTPMethod(postTestCase.method),
data: postTestCase.body,
}, postTestCase.response)
http.mockAdd({
url: getUrl(deleteTestCase.method),
method: getHTTPMethod(deleteTestCase.method),
data: deleteTestCase.body,
}, deleteTestCase.response)
})
afterEach(() => {
http.mockClear()
})
it('removes id and entity from state', async () => {
const action1 = store.dispatch(actions.post({
params: postTestCase.params,
body: postTestCase.body,
}))
await action1.payload
expect(store.getState().Crud.ids).toEqual([entity.id])
const action2 = store.dispatch(actions.delete({
params: deleteTestCase.params,
body: deleteTestCase.body,
}))
await action2.payload
expect(store.getState().Crud.ids).toEqual([])
})
})
})
})

View File

@ -1,6 +1,8 @@
import {IRoutes} from '@rondo/common'
import {IHTTPClient, ITypedRequestParams} from '../http'
export type Optional<T> = T extends {} ? T : undefined
interface ICRUDActionTypes {
readonly get: string
readonly put: string
@ -66,7 +68,7 @@ export class CRUDActions<
}
get(params: {
query: T[GET]['get']['query'],
query: Optional<T[GET]['get']['query']>,
params: T[GET]['get']['params'],
}) {
return {
@ -106,7 +108,7 @@ export class CRUDActions<
}
getMany(params: {
query: T[GET_MANY]['get']['query'],
query: Optional<T[GET_MANY]['get']['query']>,
params: T[GET_MANY]['get']['params'],
}) {
return {

View File

@ -76,7 +76,7 @@ export class CRUDReducer<T extends ICRUDIdable> {
},
}
this.actionTypes = this.getActionTypes(actionName)
this.actionTypes = this.getActionTypes()
}
protected getPromiseActionNames(type: string) {
@ -87,7 +87,8 @@ export class CRUDReducer<T extends ICRUDIdable> {
}
}
protected getActionTypes(actionName: string) {
protected getActionTypes() {
const {actionName} = this
return {
put: this.getPromiseActionNames(actionName + '_PUT'),
post: this.getPromiseActionNames(actionName + '_POST'),

View File

@ -1,3 +1,3 @@
export interface IHeader {
[key: string]: string
readonly [key: string]: string
}

View File

@ -76,7 +76,12 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
}
protected serialize(req: IRequest) {
return JSON.stringify(req, null, ' ')
return JSON.stringify({
method: req.method,
url: req.url,
params: req.params,
data: req.data,
}, null, ' ')
}
/**