Add documentation

This commit is contained in:
Jerko Steiner 2019-01-20 22:47:15 +01:00
parent 380732d28a
commit 6ee5077c88
6 changed files with 75 additions and 2 deletions

View File

@ -6,6 +6,19 @@ function isPromise(value: any): value is Promise<any> {
typeof (value as any).then === 'function' typeof (value as any).then === 'function'
} }
/**
* Handles promises returned from Actions.
*
* If `action.payload` is a `Promise`, it will be handled by this class. It
* differs from other promise middlewares for redux because by default it does
* not add an extension to action dispatched after a promise is fulfilled. This
* makes it easier to infer types from the API endpoints so they can be used in
* both Action creators and Reducers.
*
* Usage:
*
* const middleware = applyMiddleware(new PromiseMiddleware().handle)
*/
export class PromiseMiddleware { export class PromiseMiddleware {
constructor( constructor(
readonly pendingExtension = '_PENDING', readonly pendingExtension = '_PENDING',

View File

@ -3,11 +3,34 @@ import {connect, Omit} from 'react-redux'
import {Dispatch} from 'redux' import {Dispatch} from 'redux'
import {ComponentType} from 'react' import {ComponentType} from 'react'
// https://stackoverflow.com/questions/54277411 /**
* Helps isolate the component along with actions and reducer from the global
* application state.
*
* The connect() function requires a state selector, which can be used to
* select a slice of the current state to the component, which will be passed
* on to `mapStateToProps()`.
*
* Classes that extend Connector can provide dependencies in the constructor
* and then bind them to dispatch in mapDispatchToProps. This is useful to
* build components which do not depend on a "global" singletons. For example,
* the Actions class might depend on the HTTPClient class, and then it becomes
* easy to mock it during tests, or swap out different dependencies for
* different applications.
*/
export abstract class Connector { export abstract class Connector {
/**
* Connects a component using redux. The `selectState` method is used to
* select a subset of state to map to the component.
*
* It returns a component with `any` props. Ideally this could be changed to
* required props.
*
* https://stackoverflow.com/questions/54277411
*/
abstract connect<State, LocalState>( abstract connect<State, LocalState>(
getLocalState: IStateSelector<State, LocalState>, selectState: IStateSelector<State, LocalState>,
): ComponentType<any> ): ComponentType<any>
protected wrap< protected wrap<

View File

@ -1,2 +1,5 @@
/*
* Select and return a part of the state
*/
export type IStateSelector<GlobalState, StateSlice> export type IStateSelector<GlobalState, StateSlice>
= (state: GlobalState) => StateSlice = (state: GlobalState) => StateSlice

View File

@ -28,6 +28,9 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
super() super()
} }
/**
* Mock the http client.
*/
createRequestor() { createRequestor() {
return { return {
request: (req: IRequest): Promise<IResponse> => { request: (req: IRequest): Promise<IResponse> => {
@ -62,11 +65,19 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
return JSON.stringify(req, null, ' ') return JSON.stringify(req, null, ' ')
} }
/**
* Adds a new mock. If a mock with the same signature exists, it will be
* replaced. The signature is calculated using the `serialize()` method,
* which just does a `JSON.stringify(req)`.
*/
mockAdd(req: IRequest, data: any, status = 200): this { mockAdd(req: IRequest, data: any, status = 200): this {
this.mocks[this.serialize(req)] = {data, status} this.mocks[this.serialize(req)] = {data, status}
return this return this
} }
/**
* Clear all mocks and recorded requests
*/
mockClear(): this { mockClear(): this {
this.requests = [] this.requests = []
this.mocks = {} this.mocks = {}
@ -86,6 +97,17 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
waitPromise.resolve(r) waitPromise.resolve(r)
} }
/**
* Returns a new promise which will be resolve/rejected as soon as the next
* HTTP promise is resolved or rejected. Useful during testing, when the
* actual request promise is inaccessible.
*
* Example usage:
*
* TestUtils.Simulate.submit(node) // This triggers a HTTP request
* const {req, res} = await httpMock.wait()
* expect(req).toEqual({method:'get', url:'/auth/post', data: {...}})
*/
async wait(): Promise<IReqRes> { async wait(): Promise<IReqRes> {
expect(this.waitPromise).toBe(undefined) expect(this.waitPromise).toBe(undefined)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -44,6 +44,9 @@ export class TestUtils {
return combineReducers(reducers) return combineReducers(reducers)
} }
/**
* Create a redux store
*/
createStore<State, A extends Action<any> = AnyAction>( createStore<State, A extends Action<any> = AnyAction>(
params: IStoreParams<State, A>, params: IStoreParams<State, A>,
): Store<State, A> { ): Store<State, A> {
@ -55,6 +58,10 @@ export class TestUtils {
) )
} }
/**
* Creates a redux store, connects a component, and provides the `render`
* method to render the connected component with a `Provider`.
*/
withProvider<State, A extends Action<any> = AnyAction>( withProvider<State, A extends Action<any> = AnyAction>(
params: IRenderParams<State>, params: IRenderParams<State>,
) { ) {

View File

@ -1,3 +1,8 @@
/**
* Waits for a promise be rejected and return the error. If a promise resolves
* it will throw an error. To be used during testing since
* `expect(...).toThrowError()` only works with synchronous calls
*/
export async function getError(promise: Promise<any>): Promise<Error> { export async function getError(promise: Promise<any>): Promise<Error> {
let error: Error let error: Error
try { try {