Make TestUtils.tsx type safe
After spending almost two days in finding the issue, I ran across a few TypeScript issues on their GitHub page: - Loss of type inference converting to named parameters object https://github.com/Microsoft/TypeScript/issues/29791 - Parameter of a callback without a specified type next to it breaks code. https://github.com/Microsoft/TypeScript/issues/29799 - Convert to named parameters https://github.com/Microsoft/TypeScript/pull/30089 It became clear that TypeScript is unable to infer method return arguments if a generic type is used more than once in generic parameter object. Instead it returns {}. For example, the following would fail on line 28: type Convert<A, B> = (value: A) => B interface IParams<C, D> { value: C convert: Convert<C, D> doConvert: (value: C, convert: this['convert']) => D } function doSomething<E, F>(value: E, convert: Convert<E, F>) { return convert(value) } function build<G, H>(params: IParams<G, H>) { const {value, convert} = params return params.doConvert(value, convert) } const outerResult = build({ value: { a: { value: 1, }, b: 'string', }, convert: value => value.a, doConvert: (value, convert) => { const innerResult = doSomething(value, convert) innerResult.value console.log('innerResult:', innerResult) return innerResult }, }) console.log('outerResult:', outerResult) With the message: Property 'value' does not exist on type '{}'. If we replace parameter object IParams with regular ordered function parameters, the compilation succeeds. RyanCavanough (TS project lead) from GitHub commented: > We don't have a separate pass to say "Go dive into the function and > check to see if all its return statements don't rely on its parameter > type" - doing so would be expensive in light of the fact that extremely > few real-world functions actually behave like that in practice. Source: https://github.com/Microsoft/TypeScript/issues/29799#issuecomment-464154659 These modifications bring type safety to TestUtils.tsx, and therefore client-side tests of React components, while keeping almost the same ease of use as before.
This commit is contained in:
parent
0af4aa9554
commit
3595a71cec
@ -15,18 +15,21 @@ describe('LoginForm', () => {
|
||||
|
||||
const createTestProvider = () => test.withProvider({
|
||||
reducers: {Login: Feature.Login},
|
||||
connector: new Feature.LoginConnector(loginActions),
|
||||
select: state => state.Login,
|
||||
customJSX: (Component, props) =>
|
||||
<MemoryRouter><Component {...props} /></MemoryRouter>,
|
||||
})
|
||||
.withComponent(
|
||||
select => new Feature.LoginConnector(loginActions).connect(select),
|
||||
)
|
||||
.withJSX((Component, props) =>
|
||||
<MemoryRouter><Component {...props} /></MemoryRouter>,
|
||||
)
|
||||
|
||||
beforeAll(() => {
|
||||
(window as any).__MOCK_SERVER_SIDE__ = true
|
||||
})
|
||||
|
||||
it('should render', () => {
|
||||
createTestProvider().render()
|
||||
createTestProvider().render({})
|
||||
})
|
||||
|
||||
describe('submit', () => {
|
||||
|
||||
@ -13,16 +13,18 @@ describe('RegisterForm', () => {
|
||||
|
||||
const createTestProvider = () => test.withProvider({
|
||||
reducers: {Login: Feature.Login},
|
||||
connector: new Feature.RegisterConnector(loginActions),
|
||||
select: state => state.Login,
|
||||
})
|
||||
.withComponent(
|
||||
select => new Feature.RegisterConnector(loginActions).connect(select),
|
||||
)
|
||||
|
||||
beforeAll(() => {
|
||||
(window as any).__MOCK_SERVER_SIDE__ = true
|
||||
})
|
||||
|
||||
it('should render', () => {
|
||||
createTestProvider().render()
|
||||
createTestProvider().render({})
|
||||
})
|
||||
|
||||
describe('submit', () => {
|
||||
|
||||
@ -2,7 +2,8 @@ import * as Feature from './'
|
||||
// export ReactDOM from 'react-dom'
|
||||
// import T from 'react-dom/test-utils'
|
||||
import {HTTPClientMock, TestUtils/*, getError*/} from '../test-utils'
|
||||
import {IAPIDef, ITeam} from '@rondo/common'
|
||||
import {IAPIDef, ITeam, IUserInTeam} from '@rondo/common'
|
||||
import React from 'react'
|
||||
|
||||
const test = new TestUtils()
|
||||
|
||||
@ -13,18 +14,37 @@ describe('TeamConnector', () => {
|
||||
|
||||
const createTestProvider = () => test.withProvider({
|
||||
reducers: {Team: Feature.Team},
|
||||
connector: new Feature.TeamConnector(teamActions),
|
||||
select: state => state.Team,
|
||||
})
|
||||
.withComponent(select =>
|
||||
new Feature
|
||||
.TeamConnector(teamActions)
|
||||
.connect(select))
|
||||
.withJSX((Component, props) => <Component {...props} />)
|
||||
|
||||
const teams: ITeam[] = [{id: 100, name: 'my-team', userId: 1}]
|
||||
|
||||
const users: IUserInTeam[] = [{
|
||||
teamId: 123,
|
||||
userId: 1,
|
||||
displayName: 'test test',
|
||||
roleId: 1,
|
||||
roleName: 'ADMIN',
|
||||
}]
|
||||
|
||||
it('it fetches user teams on render', async () => {
|
||||
http.mockAdd({
|
||||
method: 'get',
|
||||
url: '/my/teams',
|
||||
}, teams)
|
||||
const {node} = createTestProvider().render()
|
||||
http.mockAdd({
|
||||
method: 'get',
|
||||
url: '/teams/:teamId/users',
|
||||
params: {
|
||||
teamId: 123,
|
||||
},
|
||||
}, users)
|
||||
const {node} = createTestProvider().render({editTeamId: 123})
|
||||
await http.wait()
|
||||
expect(node.innerHTML).toContain('my-team')
|
||||
})
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import T from 'react-dom/test-utils'
|
||||
import {Connector, IStateSelector} from '../redux'
|
||||
import {IStateSelector} from '../redux'
|
||||
import {Provider} from 'react-redux'
|
||||
import {createStore} from '../store'
|
||||
import {
|
||||
@ -13,15 +13,16 @@ import {
|
||||
combineReducers,
|
||||
} from 'redux'
|
||||
|
||||
interface IRenderParams<State> {
|
||||
interface IRenderParams<State, LocalState> {
|
||||
reducers: ReducersMapObject<State, any>
|
||||
state?: DeepPartial<State>
|
||||
connector: Connector<any>
|
||||
select: IStateSelector<State, any>
|
||||
customJSX?: (
|
||||
Component: React.ComponentType<any>,
|
||||
additionalProps: Record<string, any>,
|
||||
) => JSX.Element
|
||||
select: IStateSelector<State, LocalState>
|
||||
// getComponent: (
|
||||
// select: IStateSelector<State, LocalState>) => React.ComponentType<Props>,
|
||||
// customJSX?: (
|
||||
// Component: React.ComponentType<Props>,
|
||||
// props: Props,
|
||||
// ) => JSX.Element
|
||||
}
|
||||
|
||||
export class TestUtils {
|
||||
@ -47,29 +48,54 @@ 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>(
|
||||
params: IRenderParams<State>,
|
||||
withProvider<State, LocalState, A extends Action<any> = AnyAction>(
|
||||
params: IRenderParams<State, LocalState>,
|
||||
) {
|
||||
const {reducers, state, select} = params
|
||||
|
||||
const store = this.createStore({
|
||||
reducer: this.combineReducers(params.reducers),
|
||||
})(params.state)
|
||||
const Component = params.connector.connect(params.select)
|
||||
reducer: this.combineReducers(reducers),
|
||||
})(state)
|
||||
|
||||
const render = (additionalProps: Record<string, any> = {}) => {
|
||||
const jsx = params.customJSX
|
||||
? params.customJSX(Component, additionalProps)
|
||||
: <Component {...additionalProps} />
|
||||
return this.render(
|
||||
<Provider store={store}>
|
||||
{jsx}
|
||||
</Provider>,
|
||||
)
|
||||
const withComponent = <Props extends {}>(
|
||||
getComponent: (select: IStateSelector<State, LocalState>) =>
|
||||
React.ComponentType<Props>,
|
||||
) => {
|
||||
const Component = getComponent(select)
|
||||
|
||||
type CreateJSX = (
|
||||
Component: React.ComponentType<Props>,
|
||||
props: Props,
|
||||
) => JSX.Element
|
||||
|
||||
let createJSX: CreateJSX | undefined
|
||||
|
||||
const render = (props: Props) => {
|
||||
const jsx = createJSX
|
||||
? createJSX(Component, props)
|
||||
: <Component {...props} />
|
||||
return this.render(
|
||||
<Provider store={store}>
|
||||
{jsx}
|
||||
</Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
const withJSX = (localCreateJSX: CreateJSX) => {
|
||||
createJSX = localCreateJSX
|
||||
return self
|
||||
}
|
||||
|
||||
const self = {
|
||||
render,
|
||||
store,
|
||||
Component,
|
||||
withJSX,
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
return {
|
||||
render,
|
||||
store,
|
||||
Component,
|
||||
}
|
||||
return {withComponent}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user