Add start of test for crud
This commit is contained in:
parent
d6113b2fc4
commit
eed79f35f2
156
packages/client/src/crud/CRUD.test.tsx
Normal file
156
packages/client/src/crud/CRUD.test.tsx
Normal 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',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@ -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'],
|
||||
}) {
|
||||
|
||||
@ -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
|
||||
|
||||
2
packages/client/src/crud/index.ts
Normal file
2
packages/client/src/crud/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './CRUDActions'
|
||||
export * from './CRUDReducer'
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user