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 {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'],
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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)
|
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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user