Making a JSX syntax for a MockComponent and have it typed with typescript

Viewed 178

Wondering if anybody has some good suggestions on how to crack this. Got this test helper utils I have added some types to:

import { jest } from '@jest/globals'
import React from 'react'

// https://learn.reactnativeschool.com/courses/781007/lectures/14173979
export function mockComponent(moduleName: string, propOverrideFn = (props: Record<string, any>) => ({})) {
  const RealComponent = jest.requireActual(moduleName) as React.ComponentType<any>
  const CustomizedComponent = (props: Record<string, any>) => {
    return React.createElement(
      'CustomizedComponent',
      {
        ...props,
        ...propOverrideFn(props),
      },
      props.children
    )
  }
  CustomizedComponent.propTypes = RealComponent.propTypes

  return CustomizedComponent
}

So currently I can call it like this

jest.mock('react-native/Libraries/Components/Touchable/TouchableOpacity', () => {
  return mockComponent('react-native/Libraries/Components/Touchable/TouchableOpacity', (props) => {
    return {
      onPress: props.disabled ? () => {} : props.onPress
    }
  })
})

But I would like to be able to call it more like


jest.mock('react-native/Libraries/Components/Touchable/TouchableOpacity', () => {
  return <MockComponent 
          module='TouchableOpacity' 
          onPress={props => props.disabled ? () => {} : props.onPress}
         />
})

or

jest.mock('react-native/Libraries/Components/Touchable/TouchableOpacity', () => {
 return <MockComponent 
          module='TouchableOpacity'
          propOverride={props => ({onPress: props.disabled ? () => {} : props.onPress, ...props})}
        />
})
1 Answers

If you look at React without JSX, you'll see that the XML-inspired syntax (<MockComponent />) is just short for React.createElement('MockComponent').

Right now, if you renamed mockComponent to MockComponent and tried using the angle bracket syntax, the first issue is that your function receives two arguments. React components are either class components that take one constructor argument (props) or functional components that take one argument (again, props). The second issue is that your function returns a React functional component, when it needs to return a rendered React element.

One way to fix this issue is to convert mockComponent into a React functional component and make module and propOverride props of the FC.

// https://learn.reactnativeschool.com/courses/781007/lectures/14173979
export function MockComponent(props) {
  const { moduleName, propOverrideFn, ...customComponentProps } = props;

  const RealComponent = jest.requireActual(moduleName) as React.ComponentType<any>
  const CustomizedComponent = (props: Record<string, any>) => {
    return React.createElement(
      'CustomizedComponent',
      {
        ...props,
        ...propOverrideFn(props),
      },
      props.children
    )
  }
  CustomizedComponent.propTypes = RealComponent.propTypes

  return <CustomizedComponent {...customComponentProps} />
}

The differences are subtle but important. Here I modified MockComponent to take in a singular prop argument to be compatible with React.createElement(). This leads to the issue of how to differentiate props that are meant for the CustomizedComponent and what used to be arguments for mockComponent(). Here, I use the JavaScript destructuring and spread operators to separate module and propOverride from the props intended from CustomizedComponent.

Lastly, I use the JSX spread syntax to pass the arbitrary props intended for CustomizedComponent into CustomizedComponent, and I use angle brackets to render it (instead of returning a function).

I'll leave as an exercise for you to come up with the appropriate TypeScript definition for MockComponent's props. You can simply define it as the union of Record<string, any> and module and propOverride. However, you can get fancy and use a template definition so MockComponent<Toolbar> is a union of module and propOverride and Toolbar's props.

Oh, and I almost forgot. Your Jest call would look like

jest.mock('react-native/Libraries/Components/Touchable/TouchableOpacity', () => {
    (props) => {
        return <MockComponent 
            module='TouchableOpacity' 
            onPress={props => props.disabled ? () => {} : props.onPress}
            {...props}
        />
   }
})
Related