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

View File

@ -1,8 +1,7 @@
import {IAction} from '../actions'
import {indexBy, without} from '@rondo/common'
export type ICRUDMethod =
'put' | 'post' | 'delete' | 'get' | 'getMany'
export type ICRUDMethod = 'put' | 'post' | 'delete' | 'get' | 'getMany'
export interface ICRUDIdable {
readonly id: number
@ -44,7 +43,7 @@ export class CRUDReducer<T extends ICRUDIdable> {
readonly actionTypes: ReturnType<CRUDReducer<T>['getActionTypes']>
constructor(
readonly actions: ICRUDActions,
readonly actionName: string,
readonly pendingExtension = '_PENDING',
readonly resolvedExtension = '_RESOLVED',
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 {
pending: type + this.pendingExtension,
resolved: type + this.resolvedExtension,
@ -88,18 +87,17 @@ export class CRUDReducer<T extends ICRUDIdable> {
}
}
getActionTypes() {
const {actions} = this
protected getActionTypes(actionName: string) {
return {
put: this.getPromiseActionNames(actions.put),
post: this.getPromiseActionNames(actions.post),
delete: this.getPromiseActionNames(actions.delete),
get: this.getPromiseActionNames(actions.get),
getMany: this.getPromiseActionNames(actions.getMany),
put: this.getPromiseActionNames(actionName + '_PUT'),
post: this.getPromiseActionNames(actionName + '_POST'),
delete: this.getPromiseActionNames(actionName + '_DELETE'),
get: this.getPromiseActionNames(actionName + '_GET'),
getMany: this.getPromiseActionNames(actionName + '_GET_MANY'),
}
}
getUpdatedStatus(
protected getUpdatedStatus(
state: ICRUDStatus,
method: ICRUDMethod,
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
switch (actionType) {
case get.pending:
@ -133,14 +131,14 @@ export class CRUDReducer<T extends ICRUDIdable> {
}
}
getSuccessStatus(): ICRUDMethodStatus {
protected getSuccessStatus(): ICRUDMethodStatus {
return {
isLoading: false,
error: '',
}
}
reduce = (state: ICRUDState<T>, action: ICRUDAction<T | T[]>)
reduce = (state: ICRUDState<T> | undefined, action: ICRUDAction<T | T[]>)
: ICRUDState<T> => {
const {defaultState} = this
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)
if (!this.mocks.hasOwnProperty(key)) {
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)
currentRequest.finished = true
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: {...}})
*/
async wait(): Promise<IReqRes> {
if (this.requests.every(r => r.finished)) {
throw new Error('No requests to wait for')
}
expect(this.waitPromise).toBe(undefined)
const result: IReqRes = await new Promise((resolve, reject) => {
this.waitPromise = {resolve, reject}