From 3595a71cece886bbe3ac3383e200ea1a60071ef5 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Sat, 23 Mar 2019 15:48:52 +0800 Subject: [PATCH] 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 = (value: A) => B interface IParams { value: C convert: Convert doConvert: (value: C, convert: this['convert']) => D } function doSomething(value: E, convert: Convert) { return convert(value) } function build(params: IParams) { 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. --- packages/client/src/login/LoginForm.test.tsx | 11 ++- .../client/src/login/RegisterForm.test.tsx | 6 +- ...nnector.test.ts => TeamConnector.test.tsx} | 26 +++++- packages/client/src/test-utils/TestUtils.tsx | 80 ++++++++++++------- 4 files changed, 87 insertions(+), 36 deletions(-) rename packages/client/src/team/{TeamConnector.test.ts => TeamConnector.test.tsx} (56%) diff --git a/packages/client/src/login/LoginForm.test.tsx b/packages/client/src/login/LoginForm.test.tsx index 66d3abd..0a0df32 100644 --- a/packages/client/src/login/LoginForm.test.tsx +++ b/packages/client/src/login/LoginForm.test.tsx @@ -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) => - , }) + .withComponent( + select => new Feature.LoginConnector(loginActions).connect(select), + ) + .withJSX((Component, props) => + , + ) beforeAll(() => { (window as any).__MOCK_SERVER_SIDE__ = true }) it('should render', () => { - createTestProvider().render() + createTestProvider().render({}) }) describe('submit', () => { diff --git a/packages/client/src/login/RegisterForm.test.tsx b/packages/client/src/login/RegisterForm.test.tsx index 3a9ed7c..84bd021 100644 --- a/packages/client/src/login/RegisterForm.test.tsx +++ b/packages/client/src/login/RegisterForm.test.tsx @@ -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', () => { diff --git a/packages/client/src/team/TeamConnector.test.ts b/packages/client/src/team/TeamConnector.test.tsx similarity index 56% rename from packages/client/src/team/TeamConnector.test.ts rename to packages/client/src/team/TeamConnector.test.tsx index 5d7c951..30ad51b 100644 --- a/packages/client/src/team/TeamConnector.test.ts +++ b/packages/client/src/team/TeamConnector.test.tsx @@ -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) => ) 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') }) diff --git a/packages/client/src/test-utils/TestUtils.tsx b/packages/client/src/test-utils/TestUtils.tsx index dee4cf1..f265c7a 100644 --- a/packages/client/src/test-utils/TestUtils.tsx +++ b/packages/client/src/test-utils/TestUtils.tsx @@ -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 { +interface IRenderParams { reducers: ReducersMapObject state?: DeepPartial - connector: Connector - select: IStateSelector - customJSX?: ( - Component: React.ComponentType, - additionalProps: Record, - ) => JSX.Element + select: IStateSelector + // getComponent: ( + // select: IStateSelector) => React.ComponentType, + // customJSX?: ( + // Component: React.ComponentType, + // 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 = AnyAction>( - params: IRenderParams, + withProvider = AnyAction>( + params: IRenderParams, ) { + 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 = {}) => { - const jsx = params.customJSX - ? params.customJSX(Component, additionalProps) - : - return this.render( - - {jsx} - , - ) + const withComponent = ( + getComponent: (select: IStateSelector) => + React.ComponentType, + ) => { + const Component = getComponent(select) + + type CreateJSX = ( + Component: React.ComponentType, + props: Props, + ) => JSX.Element + + let createJSX: CreateJSX | undefined + + const render = (props: Props) => { + const jsx = createJSX + ? createJSX(Component, props) + : + return this.render( + + {jsx} + , + ) + } + + const withJSX = (localCreateJSX: CreateJSX) => { + createJSX = localCreateJSX + return self + } + + const self = { + render, + store, + Component, + withJSX, + } + + return self } - return { - render, - store, - Component, - } + return {withComponent} } }