Add start of test for crud

This commit is contained in:
Jerko Steiner 2019-03-28 19:35:09 +08:00
parent d6113b2fc4
commit eed79f35f2
5 changed files with 204 additions and 42 deletions

View File

@ -0,0 +1,156 @@
import React from 'react'
import {AnyAction} from 'redux'
import {CRUDActions, CRUDReducer} from './'
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
describe('CRUD', () => {
interface ITwo {
id: number
name: string
}
interface ITwoCreateBody {
name: string
}
interface ITwoListParams {
oneId: number
}
interface ITwoSpecificParams {
oneId: number
twoId: number
}
interface ITestAPI {
'/one/:oneId/two/:twoId': {
get: {
params: ITwoSpecificParams
body: ITwoCreateBody
response: ITwo
}
put: {
params: ITwoSpecificParams
body: ITwoCreateBody
response: ITwo
}
delete: {
params: ITwoSpecificParams
response: {id: number}
}
}
'/one/:oneId/two': {
get: {
params: ITwoListParams
response: ITwo[]
}
post: {
params: ITwoListParams
body: ITwoCreateBody
response: ITwo
}
}
}
const http = new HTTPClientMock<ITestAPI>()
const actions = CRUDActions.fromTwoRoutes({
http,
listRoute: '/one/:oneId/two',
specificRoute: '/one/:oneId/two/:twoId',
actionName: 'TEST',
})
const Crud = new CRUDReducer<ITwo>('TEST').reduce
const test = new TestUtils()
const reducer = test.combineReducers({
Crud,
Logger: (state: string[] = [], action: AnyAction): string[] => {
return [...state, action.type]
},
})
function getStore() {
return test.createStore({reducer})()
}
afterEach(() => {
http.mockClear()
})
describe('init', () => {
it('should not fail', () => {
getStore()
})
})
describe('GET_MANY', () => {
function getAction(store: ReturnType<typeof getStore>) {
const action = store.dispatch(actions.getMany({
query: {},
params: {oneId: 1},
}))
const state = store.getState()
expect(state.Crud.status.getMany.isLoading).toBe(true)
expect(state.Logger).toEqual([
jasmine.any(String),
'TEST_GET_MANY_PENDING',
])
return action
}
describe('TEST_GET_MANY_RESOLVED', () => {
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: {},
}, {error: 'Test Error'}, 400)
})
it('updates state', async () => {
const store = getStore()
const action = getAction(store)
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',
])
})
})
})
})

View File

@ -1,7 +1,7 @@
import {IRoutes} from '@rondo/common' import {IRoutes} from '@rondo/common'
import {IHTTPClient, ITypedRequestParams} from '../http' import {IHTTPClient, ITypedRequestParams} from '../http'
interface IActionTypes { interface ICRUDActionTypes {
readonly get: string readonly get: string
readonly put: string readonly put: string
readonly post: string readonly post: string
@ -11,21 +11,21 @@ interface IActionTypes {
export class CRUDActions< export class CRUDActions<
T extends IRoutes, T extends IRoutes,
POST extends keyof T & string,
GET_MANY extends keyof T & string,
GET extends keyof T & string, GET extends keyof T & string,
PUT extends keyof T & string, PUT extends keyof T & string,
POST extends keyof T & string,
DELETE extends keyof T & string, DELETE extends keyof T & string,
GET_MANY extends keyof T & string,
> { > {
readonly actionTypes: IActionTypes readonly actionTypes: ICRUDActionTypes
constructor( constructor(
readonly http: IHTTPClient<T>, readonly http: IHTTPClient<T>,
readonly postRoute: POST,
readonly getManyRoute: GET_MANY,
readonly getRoute: GET, readonly getRoute: GET,
readonly putRoute: PUT, readonly putRoute: PUT,
readonly postRoute: POST,
readonly deleteRoute: DELETE, readonly deleteRoute: DELETE,
readonly getManyRoute: GET_MANY,
readonly actionName: string, readonly actionName: string,
) { ) {
this.actionTypes = this.getActionTypes() this.actionTypes = this.getActionTypes()
@ -34,26 +34,27 @@ export class CRUDActions<
static fromTwoRoutes< static fromTwoRoutes<
R, R,
S extends keyof R & string, S extends keyof R & string,
P extends keyof R & string, L extends keyof R & string,
>(params: { >(params: {
http: IHTTPClient<R>, http: IHTTPClient<R>,
singular: S, specificRoute: S,
plural: P, listRoute: L,
actionName: string actionName: string,
}) { },
const {http, singular, plural, actionName} = params ) {
return new CRUDActions<R, S, S, P, S, P>( const {http, specificRoute, listRoute, actionName} = params
return new CRUDActions<R, L, L, S, S, S>(
http, http,
singular, listRoute,
singular, listRoute,
plural, specificRoute,
singular, specificRoute,
plural, specificRoute,
actionName, actionName,
) )
} }
getActionTypes(): IActionTypes { getActionTypes(): ICRUDActionTypes {
const {actionName} = this const {actionName} = this
return { return {
get: actionName + '_GET_PENDING', get: actionName + '_GET_PENDING',
@ -64,7 +65,7 @@ export class CRUDActions<
} }
} }
async get(params: { get(params: {
query: T[GET]['get']['query'], query: T[GET]['get']['query'],
params: T[GET]['get']['params'], params: T[GET]['get']['params'],
}) { }) {
@ -74,7 +75,7 @@ export class CRUDActions<
} }
} }
async post(params: { post(params: {
body: T[POST]['post']['body'], body: T[POST]['post']['body'],
params: T[POST]['post']['params'], params: T[POST]['post']['params'],
}) { }) {
@ -84,7 +85,7 @@ export class CRUDActions<
} }
} }
async put(params: { put(params: {
body: T[PUT]['put']['body'], body: T[PUT]['put']['body'],
params: T[PUT]['put']['params'], params: T[PUT]['put']['params'],
}) { }) {
@ -94,7 +95,7 @@ export class CRUDActions<
} }
} }
async delete(params: { delete(params: {
body: T[DELETE]['delete']['body'], body: T[DELETE]['delete']['body'],
params: T[DELETE]['delete']['params'], params: T[DELETE]['delete']['params'],
}) { }) {
@ -104,7 +105,7 @@ export class CRUDActions<
} }
} }
async getMany(params: { getMany(params: {
query: T[GET_MANY]['get']['query'], query: T[GET_MANY]['get']['query'],
params: T[GET_MANY]['get']['params'], params: T[GET_MANY]['get']['params'],
}) { }) {

View File

@ -1,8 +1,7 @@
import {IAction} from '../actions' import {IAction} from '../actions'
import {indexBy, without} from '@rondo/common' import {indexBy, without} from '@rondo/common'
export type ICRUDMethod = export type ICRUDMethod = 'put' | 'post' | 'delete' | 'get' | 'getMany'
'put' | 'post' | 'delete' | 'get' | 'getMany'
export interface ICRUDIdable { export interface ICRUDIdable {
readonly id: number readonly id: number
@ -44,7 +43,7 @@ export class CRUDReducer<T extends ICRUDIdable> {
readonly actionTypes: ReturnType<CRUDReducer<T>['getActionTypes']> readonly actionTypes: ReturnType<CRUDReducer<T>['getActionTypes']>
constructor( constructor(
readonly actions: ICRUDActions, readonly actionName: string,
readonly pendingExtension = '_PENDING', readonly pendingExtension = '_PENDING',
readonly resolvedExtension = '_RESOLVED', readonly resolvedExtension = '_RESOLVED',
readonly rejectedExtension = '_REJECTED', readonly rejectedExtension = '_REJECTED',
@ -77,10 +76,10 @@ export class CRUDReducer<T extends ICRUDIdable> {
}, },
} }
this.actionTypes = this.getActionTypes() this.actionTypes = this.getActionTypes(actionName)
} }
getPromiseActionNames(type: string) { protected getPromiseActionNames(type: string) {
return { return {
pending: type + this.pendingExtension, pending: type + this.pendingExtension,
resolved: type + this.resolvedExtension, resolved: type + this.resolvedExtension,
@ -88,18 +87,17 @@ export class CRUDReducer<T extends ICRUDIdable> {
} }
} }
getActionTypes() { protected getActionTypes(actionName: string) {
const {actions} = this
return { return {
put: this.getPromiseActionNames(actions.put), put: this.getPromiseActionNames(actionName + '_PUT'),
post: this.getPromiseActionNames(actions.post), post: this.getPromiseActionNames(actionName + '_POST'),
delete: this.getPromiseActionNames(actions.delete), delete: this.getPromiseActionNames(actionName + '_DELETE'),
get: this.getPromiseActionNames(actions.get), get: this.getPromiseActionNames(actionName + '_GET'),
getMany: this.getPromiseActionNames(actions.getMany), getMany: this.getPromiseActionNames(actionName + '_GET_MANY'),
} }
} }
getUpdatedStatus( protected getUpdatedStatus(
state: ICRUDStatus, state: ICRUDStatus,
method: ICRUDMethod, method: ICRUDMethod,
status: ICRUDMethodStatus, status: ICRUDMethodStatus,
@ -110,7 +108,7 @@ export class CRUDReducer<T extends ICRUDIdable> {
} }
} }
getMethod(actionType: string): ICRUDMethod { protected getMethod(actionType: string): ICRUDMethod {
const {get, put, post, delete: _delete, getMany} = this.actionTypes const {get, put, post, delete: _delete, getMany} = this.actionTypes
switch (actionType) { switch (actionType) {
case get.pending: case get.pending:
@ -133,14 +131,14 @@ export class CRUDReducer<T extends ICRUDIdable> {
} }
} }
getSuccessStatus(): ICRUDMethodStatus { protected getSuccessStatus(): ICRUDMethodStatus {
return { return {
isLoading: false, isLoading: false,
error: '', error: '',
} }
} }
reduce = (state: ICRUDState<T>, action: ICRUDAction<T | T[]>) reduce = (state: ICRUDState<T> | undefined, action: ICRUDAction<T | T[]>)
: ICRUDState<T> => { : ICRUDState<T> => {
const {defaultState} = this const {defaultState} = this
state = state || defaultState state = state || defaultState

View File

@ -0,0 +1,2 @@
export * from './CRUDActions'
export * from './CRUDReducer'

View File

@ -48,7 +48,9 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
const key = this.serialize(req) const key = this.serialize(req)
if (!this.mocks.hasOwnProperty(key)) { if (!this.mocks.hasOwnProperty(key)) {
setImmediate(() => { setImmediate(() => {
const err = new Error('No mock for request: ' + key) const err = new Error(
'No mock for request: ' + key + '\nAvailable mocks:' +
Object.keys(this.mocks))
reject(err) reject(err)
currentRequest.finished = true currentRequest.finished = true
this.notify(err) this.notify(err)
@ -124,6 +126,9 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
* expect(req).toEqual({method:'get', url:'/auth/post', data: {...}}) * expect(req).toEqual({method:'get', url:'/auth/post', data: {...}})
*/ */
async wait(): Promise<IReqRes> { async wait(): Promise<IReqRes> {
if (this.requests.every(r => r.finished)) {
throw new Error('No requests to wait for')
}
expect(this.waitPromise).toBe(undefined) expect(this.waitPromise).toBe(undefined)
const result: IReqRes = await new Promise((resolve, reject) => { const result: IReqRes = await new Promise((resolve, reject) => {
this.waitPromise = {resolve, reject} this.waitPromise = {resolve, reject}