diff --git a/jest.config.js b/jest.config.js index 2e82e40f9..95e8fdb12 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,18 +1,13 @@ +const { defaults: tsjPreset } = require('ts-jest/presets') + const defaults = { coverageDirectory: './coverage/', collectCoverage: true, testURL: 'http://localhost', } -const testFolderPath = (folderName) => `/test/${folderName}/**/*.js` const NORMAL_TEST_FOLDERS = ['components', 'hooks', 'integration', 'utils'] -const standardConfig = { - ...defaults, - displayName: 'ReactDOM', - testMatch: NORMAL_TEST_FOLDERS.map(testFolderPath), -} - const tsTestFolderPath = (folderName) => `/test/${folderName}/**/*.{ts,tsx}` @@ -26,13 +21,14 @@ const tsStandardConfig = { const rnConfig = { ...defaults, displayName: 'React Native', - testMatch: [testFolderPath('react-native')], + testMatch: [tsTestFolderPath('react-native')], preset: 'react-native', transform: { '^.+\\.js$': '/node_modules/react-native/jest/preprocessor.js', + ...tsjPreset.transform, }, } module.exports = { - projects: [tsStandardConfig, standardConfig, rnConfig], + projects: [tsStandardConfig, rnConfig], } diff --git a/package.json b/package.json index 67bd09605..de228a83c 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@types/react": "^17.0.14", "@types/react-dom": "^17.0.9", "@types/react-is": "^17.0.1", + "@types/react-native": "^0.64.12", "@types/react-redux": "^7.1.18", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", diff --git a/test/react-native/batch-integration.js b/test/react-native/batch-integration.tsx similarity index 69% rename from test/react-native/batch-integration.js rename to test/react-native/batch-integration.tsx index bc28f51ab..baae8589c 100644 --- a/test/react-native/batch-integration.js +++ b/test/react-native/batch-integration.tsx @@ -12,8 +12,10 @@ import { useIsomorphicLayoutEffect } from '../../src/utils/useIsomorphicLayoutEf import * as rtl from '@testing-library/react-native' import '@testing-library/jest-native/extend-expect' +import type { MiddlewareAPI, Dispatch as ReduxDispatch } from 'redux' + describe('React Native', () => { - const propMapper = (prop) => { + const propMapper = (prop: any) => { switch (typeof prop) { case 'object': case 'boolean': @@ -24,12 +26,16 @@ describe('React Native', () => { return prop } } - class Passthrough extends Component { + + interface PassthroughPropsType { + [x: string]: any + } + class Passthrough extends Component { render() { return ( {Object.keys(this.props).map((prop) => ( - + {propMapper(this.props[prop])} ))} @@ -37,8 +43,11 @@ describe('React Native', () => { ) } } - - function stringBuilder(prev = '', action) { + interface ActionType { + type: string + body?: string + } + function stringBuilder(prev = '', action: ActionType) { return action.type === 'APPEND' ? prev + action.body : prev } @@ -58,6 +67,9 @@ describe('React Native', () => { describe('Subscription and update timing correctness', () => { it('should pass state consistently to mapState', () => { + type RootStateType = string + type NoDispatch = {} + const store = createStore(stringBuilder) rtl.act(() => { @@ -65,9 +77,11 @@ describe('React Native', () => { }) let childMapStateInvokes = 0 - - @connect((state) => ({ state })) - class Container extends Component { + interface ContainerTStatePropsType { + state: RootStateType + } + type ContainerOwnOwnPropsType = {} + class Container extends Component { emitChange() { store.dispatch({ type: 'APPEND', body: 'b' }) } @@ -80,29 +94,46 @@ describe('React Native', () => { testID="change-button" onPress={this.emitChange.bind(this)} /> - + ) } } + const ConnectedContainer = connect< + ContainerTStatePropsType, + NoDispatch, + ContainerOwnOwnPropsType, + RootStateType + >((state) => ({ state }))(Container) + + const childCalls: Array<[string, string]> = [] + + type ChildrenTStatePropsType = {} + type ChildrenOwnPropsType = { + parentState: string + } - const childCalls = [] - @connect((state, parentProps) => { - childMapStateInvokes++ - childCalls.push([state, parentProps.parentState]) - // The state from parent props should always be consistent with the current state - expect(state).toEqual(parentProps.parentState) - return {} - }) class ChildContainer extends Component { render() { return } } + const ConnectedChildrenContainer = connect< + ChildrenTStatePropsType, + NoDispatch, + ChildrenOwnPropsType, + RootStateType + >((state, parentProps) => { + childMapStateInvokes++ + childCalls.push([state, parentProps.parentState]) + // The state from parent props should always be consistent with the current state + expect(state).toEqual(parentProps.parentState) + return {} + })(ChildContainer) const tester = rtl.render( - + ) @@ -140,42 +171,58 @@ describe('React Native', () => { // Explicitly silence "not wrapped in act()" messages for this test const spy = jest.spyOn(console, 'error') spy.mockImplementation(() => {}) + type RootStateType = number + type NoDispatch = {} + const store = createStore((state: RootStateType = 0) => state + 1) - const store = createStore((state = 0) => state + 1) - - let propsPassedIn + interface TStatePropsType { + reduxCount: number + } + interface OwnPropsType { + count: number + } + let propsPassedIn: TStatePropsType & OwnPropsType - @connect((reduxCount) => { - return { reduxCount } - }) - class InnerComponent extends Component { + class InnerComponent extends Component { render() { propsPassedIn = this.props return } } + const ConnectedInner = connect< + TStatePropsType, + NoDispatch, + OwnPropsType, + RootStateType + >((reduxCount) => { + return { reduxCount } + })(InnerComponent) - class OuterComponent extends Component { - constructor() { - super() + type OutStateType = { + count: number + } + class OuterComponent extends Component<{}, OutStateType> { + constructor(props: {}) { + super(props) this.state = { count: 0 } } render() { - return + return } } - let outerComponent + let outerComponent = React.createRef() rtl.render( - (outerComponent = c)} /> + ) - outerComponent.setState(({ count }) => ({ count: count + 1 })) + outerComponent.current!.setState(({ count }) => ({ count: count + 1 })) store.dispatch({ type: '' }) - + //@ts-ignore expect(propsPassedIn.count).toEqual(1) + //@ts-ignore expect(propsPassedIn.reduxCount).toEqual(2) spy.mockRestore() @@ -185,13 +232,16 @@ describe('React Native', () => { // Explicitly silence "not wrapped in act()" messages for this test const spy = jest.spyOn(console, 'error') spy.mockImplementation(() => {}) + type ActionType = { + type: string + payload?: () => void + } + const reactCallbackMiddleware = (store: MiddlewareAPI) => { + let callback: () => void - const reactCallbackMiddleware = (store) => { - let callback - - return (next) => (action) => { + return (next: ReduxDispatch) => (action: ActionType) => { if (action.type === 'SET_COMPONENT_CALLBACK') { - callback = action.payload + callback = action.payload! } if (callback && action.type === 'INC1') { @@ -213,7 +263,9 @@ describe('React Native', () => { } } - const counter = (state = 0, action) => { + type RootStateType = number + + const counter = (state: RootStateType = 0, action: ActionType) => { if (action.type === 'INC1') { return state + 1 } else if (action.type === 'INC2') { @@ -227,7 +279,22 @@ describe('React Native', () => { applyMiddleware(reactCallbackMiddleware) ) - const Child = connect((count) => ({ count }))(function (props) { + interface ChildrenTStatePropsType { + count: RootStateType + } + type NoDispatch = {} + type OwnPropsType = { + prop: string + } + + const Child = connect< + ChildrenTStatePropsType, + NoDispatch, + OwnPropsType, + RootStateType + >((count) => ({ count }))(function ( + props: OwnPropsType & ChildrenTStatePropsType + ) { return ( {props.prop} @@ -235,9 +302,13 @@ describe('React Native', () => { ) }) - class Parent extends Component { - constructor() { - super() + interface ParentPropsType { + prop: string + } + class Parent extends Component<{}, ParentPropsType> { + inc1: () => void + constructor(props: {}) { + super(props) this.state = { prop: 'a', } @@ -257,13 +328,13 @@ describe('React Native', () => { } } - let parent - const rendered = rtl.render( (parent = ref)} />) + let parent = React.createRef() + const rendered = rtl.render() expect(rendered.getByTestId('child-count').children).toEqual(['0']) expect(rendered.getByTestId('child-prop').children).toEqual(['a']) // Force the multi-update sequence by running this bound action creator - parent.inc1() + parent.current!.inc1() // The connected child component _should_ have rendered with the latest Redux // store value (3) _and_ the latest wrapper prop ('b'). @@ -277,53 +348,76 @@ describe('React Native', () => { // Explicitly silence "not wrapped in act()" messages for this test const spy = jest.spyOn(console, 'error') spy.mockImplementation(() => {}) - const store = createStore((state = 0) => state + 1) + type RootStateType = number + const store = createStore((state: RootStateType = 0) => state + 1) let reduxCountPassedToMapState - @connect((reduxCount) => { - reduxCountPassedToMapState = reduxCount - return reduxCount < 2 ? { a: 'a' } : { a: 'b' } - }) class InnerComponent extends Component { render() { return } } - class OuterComponent extends Component { - constructor() { - super() + interface InnerTStatePropsType { + a: string + } + type NoDispatch = {} + type InnerOwnPropsType = { + count: number + } + + const ConnectedInner = connect< + InnerTStatePropsType, + NoDispatch, + InnerOwnPropsType, + RootStateType + >((reduxCount) => { + reduxCountPassedToMapState = reduxCount + return reduxCount < 2 ? { a: 'a' } : { a: 'b' } + })(InnerComponent) + + interface OuterState { + count: number + } + class OuterComponent extends Component<{}, OuterState> { + constructor(props: {}) { + super(props) this.state = { count: 0 } } render() { - return + return } } - let outerComponent + let outerComponent = React.createRef() rtl.render( - (outerComponent = c)} /> + ) store.dispatch({ type: '' }) store.dispatch({ type: '' }) - outerComponent.setState(({ count }) => ({ count: count + 1 })) + outerComponent.current!.setState(({ count }) => ({ count: count + 1 })) expect(reduxCountPassedToMapState).toEqual(3) spy.mockRestore() }) - it('should ensure top-down updates for consecutive batched updates', () => { + it('1should ensure top-down updates for consecutive batched updates', () => { const INC = 'INC' - const reducer = (c = 0, { type }) => (type === INC ? c + 1 : c) + type ActionType = { + type: string + } + type RootStateType = number + const reducer = (c: RootStateType = 0, { type }: ActionType) => + type === INC ? c + 1 : c const store = createStore(reducer) - let executionOrder = [] + let executionOrder: string[] = [] let expectedExecutionOrder = [ 'parent map', 'parent render', @@ -378,8 +472,17 @@ describe('React Native', () => { spy.mockImplementation(() => {}) const INIT_STATE = { bool: false } + type ActionType = { + type: string + } + interface RootStateType { + bool: boolean + } - const reducer = (state = INIT_STATE, action) => { + const reducer = ( + state: RootStateType = INIT_STATE, + action: ActionType + ) => { switch (action.type) { case 'TOGGLE': return { bool: !state.bool } @@ -390,7 +493,7 @@ describe('React Native', () => { const store = createStore(reducer, INIT_STATE) - const selector = (state) => ({ + const selector = (state: RootStateType) => ({ bool: state.bool, }) @@ -467,25 +570,27 @@ describe('React Native', () => { const rendered = rtl.render() - const assertValuesMatch = (rendered) => { + type RenderedType = typeof rendered + + const assertValuesMatch = (rendered: RenderedType) => { const [, boolFromSelector] = rendered.getByTestId('boolFromSelector').children const [, boolFromStore] = rendered.getByTestId('boolFromStore').children expect(boolFromSelector).toBe(boolFromStore) } - const clickButton = (rendered, testID) => { + const clickButton = (rendered: RenderedType, testID: string) => { const button = rendered.getByTestId(testID) rtl.fireEvent.press(button) } - const clickAndRender = (rendered, testID) => { + const clickAndRender = (rendered: RenderedType, testId: string) => { // Note: Normally we'd wrap this all in act(), but that automatically // wraps your code in batchedUpdates(). The point of this bug is that it // specifically occurs when you are _not_ batching updates! - clickButton(rendered, 'setTimeout') + clickButton(rendered, testId) jest.advanceTimersByTime(100) - assertValuesMatch(rendered, testID) + assertValuesMatch(rendered) } assertValuesMatch(rendered) diff --git a/types/index.d.ts b/types/index.d.ts index b27ede89d..cfaec7ba2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,18 +1,5 @@ /* eslint-disable no-unused-vars */ -declare module 'react-native' { - export function unstable_batchedUpdates( - callback: (a: A, b: B) => any, - a: A, - b: B - ): void - export function unstable_batchedUpdates( - callback: (a: A) => any, - a: A - ): void - export function unstable_batchedUpdates(callback: () => any): void -} - declare module 'react-is' { import * as React from 'react' export function isContextConsumer(value: any): value is React.ReactElement diff --git a/yarn.lock b/yarn.lock index 6ae137a98..cc2ebec1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3589,6 +3589,15 @@ __metadata: languageName: node linkType: hard +"@types/react-native@npm:^0.64.12": + version: 0.64.12 + resolution: "@types/react-native@npm:0.64.12" + dependencies: + "@types/react": "*" + checksum: de9dd20a86d02b1e8715c55ec019c3062e9f1e52e4b09e4fb00f9ecd017b684c607871fc700722abfeca9b69820703871527db43170758a1984702192c34013a + languageName: node + linkType: hard + "@types/react-redux@npm:^7.1.18": version: 7.1.18 resolution: "@types/react-redux@npm:7.1.18" @@ -14161,6 +14170,7 @@ __metadata: "@types/react": ^17.0.14 "@types/react-dom": ^17.0.9 "@types/react-is": ^17.0.1 + "@types/react-native": ^0.64.12 "@types/react-redux": ^7.1.18 "@typescript-eslint/eslint-plugin": ^4.28.0 "@typescript-eslint/parser": ^4.28.0