callstack / react-native-pager-view

React Native wrapper for the Android ViewPager and iOS UIPageViewController.
MIT License
2.69k stars 413 forks source link

How to mock this library with jest? (Fails when used normally) #481

Open princefishthrower opened 2 years ago

princefishthrower commented 2 years ago

Environment

Relevant versions:

"react": "17.0.2",
"react-native": "0.66.0",
"react-native-pager-view": "^5.4.9",

Description

Calling the setPage method on a viewPager ref causes jest to throw this error:

TypeError: Cannot read properties of undefined (reading 'Commands')

      87 |     if (viewPager.current) {
    > 88 |       viewPager.current.setPage(page);
         |                         ^
      89 |     }
      90 | 

Reproducible Demo

  1. Create an integration test with any component using ViewPager and call setPage on it's ref. This error will throw.
princefishthrower commented 2 years ago

Anybody? This is like the last thing I need to mock / stub to get my integration tests to start fully working...

troZee commented 2 years ago
jest.mock('react-native-pager-view', () => {
  const React = require('react');
  const View = require('react-native').View;

  return class ViewPager extends React.Component {
    render() {
      const {
        children,
        initialPage,
        onPageScroll,
        onPageScrollStateChanged,
        onPageSelected,
        style,
        scrollEnabled,
        accessibilityLabel,
      } = this.props;
      console.log({
        children,
        initialPage,
        onPageScroll,
        onPageScrollStateChanged,
        onPageSelected,
        style,
        scrollEnabled,
        accessibilityLabel,
      });
      return (
        <View
          testID={this.props.testID}
          initialPage={initialPage}
          onPageScroll={onPageScroll}
          onPageScrollStateChanged={onPageScrollStateChanged}
          onPageSelected={onPageSelected}
          style={style}
          scrollEnabled={scrollEnabled}
          accessibilityLabel={accessibilityLabel}
        >
          {children}
        </View>
      );
    }
  };
});
madflanderz commented 2 years ago

I extended the example with the public class methods and now it is working:

jest.mock("react-native-pager-view", () => {
  const React = require("react");
  const View = require("react-native").View;

  return class ViewPager extends React.Component {
    // *********************
    // THIS WAS MISSING
    setPage() {}
    setPageWithoutAnimation() {}
    setScrollEnabled() {}
    // *********************

    render() {
      const {
        children,
        initialPage,
        onPageScroll,
        onPageScrollStateChanged,
        onPageSelected,
        style,
        scrollEnabled,
        accessibilityLabel,
      } = this.props;

      console.log({
        children,
        initialPage,
        onPageScroll,
        onPageScrollStateChanged,
        onPageSelected,
        style,
        scrollEnabled,
        accessibilityLabel,
      });

      return (
        <View
          testID={this.props.testID}
          initialPage={initialPage}
          onPageScroll={onPageScroll}
          onPageScrollStateChanged={onPageScrollStateChanged}
          onPageSelected={onPageSelected}
          style={style}
          scrollEnabled={scrollEnabled}
          accessibilityLabel={accessibilityLabel}>
          {children}
        </View>
      );
    }
  };
});
mowbell commented 2 years ago

You can complement setPage callback mock and allow page view switching with: setPage(selectedPage) { this.props.onPageSelected({ nativeEvent: { position: selectedPage } }); } It allows rendering of content when a tab is pressed

shubhnik commented 2 years ago

I had a use-case where I wanted to test screen change using setPage. Made some changes to get it working for me:

jest.mock('react-native-pager-view', () => {
  const React = require('react');
  const PropTypes = require('prop-types');
  const View = require('react-native').View;

  class ViewPager extends React.Component {
    constructor(props) {
      super();
      this.state = {
        page: props.initialPage,
      };
    }

    setPage(selectedPage) {
      this.setState({ page: selectedPage });
    }
    setPageWithoutAnimation() {}
    setScrollEnabled() {}

    render() {
      const {
        children,
        initialPage,
        onPageScroll,
        onPageScrollStateChanged,
        onPageSelected,
        style,
        scrollEnabled,
        accessibilityLabel,
      } = this.props;

      return (
        <View
          testID={this.props.testID}
          initialPage={initialPage}
          onPageScroll={onPageScroll}
          onPageScrollStateChanged={onPageScrollStateChanged}
          onPageSelected={onPageSelected}
          style={style}
          scrollEnabled={scrollEnabled}
          accessibilityLabel={accessibilityLabel}
        >
          {children[this.state.page]}
        </View>
      );
    }
  }
  ViewPager.propTypes = {
    children: PropTypes.element,
    initialPage: PropTypes.number,
    onPageScroll: PropTypes.func,
    onPageScrollStateChanged: PropTypes.func,
    onPageSelected: PropTypes.func,
    style: PropTypes.object,
    scrollEnabled: PropTypes.bool,
    accessibilityLabel: PropTypes.string,
    testID: PropTypes.string,
  };

  return ViewPager;
});
helferleinsoftware commented 1 year ago

Thank you very much, @shubhnik . I needed to change setPage to

    setPage(selectedPage: number) {
      this.setState({ page: selectedPage });
      this.props.onPageSelected({ nativeEvent: { position: selectedPage } } as Partial<PagerViewOnPageSelectedEvent>);
    }

to make it working :)

MarianPalkus commented 1 year ago

Thanks @shubhnik and @helferleinsoftware.

In my case it was useful to define setPageWithoutAnimation as well:

  setPageWithoutAnimation(selectedPage) {
    this.setState({ page: selectedPage });
    this.props.onPageSelected({ nativeEvent: { position: selectedPage } });
  }

I also got this warning:

 Warning: Failed prop type: Invalid prop `children` of type `array` supplied to `ViewPager`, expected a single ReactElement.

which can be avoided by adjusting the prop types for children:

-- children: PropTypes.element,
++ children: PropTypes.node,