Skip to content

Commit 3595a71

Browse files
committed
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 microsoft/TypeScript#29791 - Parameter of a callback without a specified type next to it breaks code. microsoft/TypeScript#29799 - Convert to named parameters microsoft/TypeScript#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: microsoft/TypeScript#29799 (comment) 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.
1 parent 0af4aa9 commit 3595a71

File tree

4 files changed

+87
-36
lines changed

4 files changed

+87
-36
lines changed

packages/client/src/login/LoginForm.test.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@ describe('LoginForm', () => {
1515

1616
const createTestProvider = () => test.withProvider({
1717
reducers: {Login: Feature.Login},
18-
connector: new Feature.LoginConnector(loginActions),
1918
select: state => state.Login,
20-
customJSX: (Component, props) =>
21-
<MemoryRouter><Component {...props} /></MemoryRouter>,
2219
})
20+
.withComponent(
21+
select => new Feature.LoginConnector(loginActions).connect(select),
22+
)
23+
.withJSX((Component, props) =>
24+
<MemoryRouter><Component {...props} /></MemoryRouter>,
25+
)
2326

2427
beforeAll(() => {
2528
(window as any).__MOCK_SERVER_SIDE__ = true
2629
})
2730

2831
it('should render', () => {
29-
createTestProvider().render()
32+
createTestProvider().render({})
3033
})
3134

3235
describe('submit', () => {

packages/client/src/login/RegisterForm.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ describe('RegisterForm', () => {
1313

1414
const createTestProvider = () => test.withProvider({
1515
reducers: {Login: Feature.Login},
16-
connector: new Feature.RegisterConnector(loginActions),
1716
select: state => state.Login,
1817
})
18+
.withComponent(
19+
select => new Feature.RegisterConnector(loginActions).connect(select),
20+
)
1921

2022
beforeAll(() => {
2123
(window as any).__MOCK_SERVER_SIDE__ = true
2224
})
2325

2426
it('should render', () => {
25-
createTestProvider().render()
27+
createTestProvider().render({})
2628
})
2729

2830
describe('submit', () => {

packages/client/src/team/TeamConnector.test.ts renamed to packages/client/src/team/TeamConnector.test.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import * as Feature from './'
22
// export ReactDOM from 'react-dom'
33
// import T from 'react-dom/test-utils'
44
import {HTTPClientMock, TestUtils/*, getError*/} from '../test-utils'
5-
import {IAPIDef, ITeam} from '@rondo/common'
5+
import {IAPIDef, ITeam, IUserInTeam} from '@rondo/common'
6+
import React from 'react'
67

78
const test = new TestUtils()
89

@@ -13,18 +14,37 @@ describe('TeamConnector', () => {
1314

1415
const createTestProvider = () => test.withProvider({
1516
reducers: {Team: Feature.Team},
16-
connector: new Feature.TeamConnector(teamActions),
1717
select: state => state.Team,
1818
})
19+
.withComponent(select =>
20+
new Feature
21+
.TeamConnector(teamActions)
22+
.connect(select))
23+
.withJSX((Component, props) => <Component {...props} />)
1924

2025
const teams: ITeam[] = [{id: 100, name: 'my-team', userId: 1}]
2126

27+
const users: IUserInTeam[] = [{
28+
teamId: 123,
29+
userId: 1,
30+
displayName: 'test test',
31+
roleId: 1,
32+
roleName: 'ADMIN',
33+
}]
34+
2235
it('it fetches user teams on render', async () => {
2336
http.mockAdd({
2437
method: 'get',
2538
url: '/my/teams',
2639
}, teams)
27-
const {node} = createTestProvider().render()
40+
http.mockAdd({
41+
method: 'get',
42+
url: '/teams/:teamId/users',
43+
params: {
44+
teamId: 123,
45+
},
46+
}, users)
47+
const {node} = createTestProvider().render({editTeamId: 123})
2848
await http.wait()
2949
expect(node.innerHTML).toContain('my-team')
3050
})
Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import ReactDOM from 'react-dom'
33
import T from 'react-dom/test-utils'
4-
import {Connector, IStateSelector} from '../redux'
4+
import {IStateSelector} from '../redux'
55
import {Provider} from 'react-redux'
66
import {createStore} from '../store'
77
import {
@@ -13,15 +13,16 @@ import {
1313
combineReducers,
1414
} from 'redux'
1515

16-
interface IRenderParams<State> {
16+
interface IRenderParams<State, LocalState> {
1717
reducers: ReducersMapObject<State, any>
1818
state?: DeepPartial<State>
19-
connector: Connector<any>
20-
select: IStateSelector<State, any>
21-
customJSX?: (
22-
Component: React.ComponentType<any>,
23-
additionalProps: Record<string, any>,
24-
) => JSX.Element
19+
select: IStateSelector<State, LocalState>
20+
// getComponent: (
21+
// select: IStateSelector<State, LocalState>) => React.ComponentType<Props>,
22+
// customJSX?: (
23+
// Component: React.ComponentType<Props>,
24+
// props: Props,
25+
// ) => JSX.Element
2526
}
2627

2728
export class TestUtils {
@@ -47,29 +48,54 @@ export class TestUtils {
4748
* Creates a redux store, connects a component, and provides the `render`
4849
* method to render the connected component with a `Provider`.
4950
*/
50-
withProvider<State, A extends Action<any> = AnyAction>(
51-
params: IRenderParams<State>,
51+
withProvider<State, LocalState, A extends Action<any> = AnyAction>(
52+
params: IRenderParams<State, LocalState>,
5253
) {
54+
const {reducers, state, select} = params
55+
5356
const store = this.createStore({
54-
reducer: this.combineReducers(params.reducers),
55-
})(params.state)
56-
const Component = params.connector.connect(params.select)
57+
reducer: this.combineReducers(reducers),
58+
})(state)
5759

58-
const render = (additionalProps: Record<string, any> = {}) => {
59-
const jsx = params.customJSX
60-
? params.customJSX(Component, additionalProps)
61-
: <Component {...additionalProps} />
62-
return this.render(
63-
<Provider store={store}>
64-
{jsx}
65-
</Provider>,
66-
)
67-
}
60+
const withComponent = <Props extends {}>(
61+
getComponent: (select: IStateSelector<State, LocalState>) =>
62+
React.ComponentType<Props>,
63+
) => {
64+
const Component = getComponent(select)
65+
66+
type CreateJSX = (
67+
Component: React.ComponentType<Props>,
68+
props: Props,
69+
) => JSX.Element
70+
71+
let createJSX: CreateJSX | undefined
6872

69-
return {
70-
render,
71-
store,
72-
Component,
73+
const render = (props: Props) => {
74+
const jsx = createJSX
75+
? createJSX(Component, props)
76+
: <Component {...props} />
77+
return this.render(
78+
<Provider store={store}>
79+
{jsx}
80+
</Provider>,
81+
)
82+
}
83+
84+
const withJSX = (localCreateJSX: CreateJSX) => {
85+
createJSX = localCreateJSX
86+
return self
87+
}
88+
89+
const self = {
90+
render,
91+
store,
92+
Component,
93+
withJSX,
94+
}
95+
96+
return self
7397
}
98+
99+
return {withComponent}
74100
}
75101
}

0 commit comments

Comments
 (0)