How to mock .measure in jest?
Asked Answered
P

2

7

I am trying to test my component. In my component, there is a function to measure from the reference of View. when I debug, the ref value exists. but the measure method is undefined.

This is the screenshot of the testing.

enter image description here

I need help how to mock the .measure or other function from the component reference

Component:

import React, {useState, useCallback, useMemo, useRef} from 'react';
import {
  FlatListProps,
  Animated,
  StyleSheet,
  View,
  FlatList,
  ViewStyle,
  TextStyle,
  ListRenderItemInfo,
} from 'react-native';
import {Layout} from '../../layout';
import {useDidUpdate} from '../../utilities';
import {Icon} from '../icon';
import {Touchable} from '../touchable';
import {Modal} from '../modal';
import {Text} from '../text';
import {PickerSelectionInfo} from '../../types';

export interface PickerProps<ItemT> extends FlatListProps<ItemT> {
  containerStyle?: ViewStyle;
  titleStyle?: TextStyle;
  titleContainerStyle?: ViewStyle;
  selected?: string;
  selectedContainerStyle?: ViewStyle;
  selectedTitleStyle?: TextStyle;
  listContainerStyle?: ViewStyle;
  listItemContainerStyle?: ViewStyle;
  listItemSelectedContainerStyle?: ViewStyle;
  fullWidth?: boolean;
  animationDuration?: number;
  placeholder?: string;
  placeholderStyle?: TextStyle;
  icon?: JSX.Element;
  iconContainerStyle?: ViewStyle;
  iconStartRotation?: string;
  iconEndRotation?: string;
  data: ReadonlyArray<ItemT>;
  keyExtractor(item: ItemT, index: number): string;
  titleExtractor?(item: ItemT, index: number): string;
  onSelect(select: PickerSelectionInfo<ItemT>): void;
}

export default function Picker<ItemT>({
  containerStyle,
  titleStyle,
  titleContainerStyle,
  selected,
  selectedContainerStyle,
  selectedTitleStyle,
  listContainerStyle,
  listItemContainerStyle,
  listItemSelectedContainerStyle,
  fullWidth = false,
  animationDuration = 250,
  placeholder = 'Select Option',
  placeholderStyle,
  icon,
  iconContainerStyle,
  iconStartRotation = '-90deg',
  iconEndRotation = '0deg',
  data,
  keyExtractor,
  titleExtractor,
  onSelect,
  renderItem,
  ...props
}: PickerProps<ItemT>) {
  const [selection, setSelection] = useState<
    PickerSelectionInfo<ItemT> | undefined
  >(getInfoFromKey(selected));
  const [toggle, setToggle] = useState(false);
  const layout = useRef<Layout>();
  const refButton = useRef<View>();
  const animation = useState(new Animated.Value(0))[0];

  function getInfoFromKey(
    key?: string,
  ): PickerSelectionInfo<ItemT> | undefined {
    if (key !== undefined) {
      for (let index = 0; index < data.length; index++) {
        const item = data[index];

        if (keyExtractor(item, index) === key) {
          return {key, index, item};
        }
      }
    }

    return undefined;
  }

  const handleRefButton = useCallback((instance: View | null) => {
    if (instance) {
      refButton.current = instance;
    }
  }, []);

  const handleRunAnimation = useCallback(
    () =>
      Animated.timing(animation, {
        toValue: toggle ? 1 : 0,
        duration: animationDuration,
        useNativeDriver: true,
      }).start(),
    [animation, toggle, animationDuration],
  );

  const handlePressMenuItem = useCallback(
    (key: string, item: ItemT, index: number) => {
      setSelection({key, item, index});
      setToggle(false);
      onSelect({key, item, index});
    },
    [],
  );

  const handlePressButton = useCallback(
    () =>
      refButton.current?.measure((x, y, width, height, pageX, pageY) => {
        layout.current = {
          x,
          y,
          width,
          height,
          pageX,
          pageY,
        };
        setToggle(!toggle);
      }),
    [refButton.current, toggle],
  );

  const handleRenderMenuItem = useCallback(
    (info: ListRenderItemInfo<ItemT>) => {
      const {item, index} = info;
      const key = keyExtractor(item, index);

      return (
        <Touchable
          style={StyleSheet.flatten([
            styles.listItemContainer,
            listItemContainerStyle,
            selection?.index === index &&
              StyleSheet.flatten([
                styles.listItemSelectedContainer,
                listItemSelectedContainerStyle,
              ]),
          ])}
          onPress={() => handlePressMenuItem(key, item, index)}>
          {renderItem(info)}
        </Touchable>
      );
    },
    [
      selection,
      listItemContainerStyle,
      listItemSelectedContainerStyle,
      keyExtractor,
      renderItem,
      handlePressMenuItem,
    ],
  );

  const handleRenderIcon = useMemo(
    () => (
      <Animated.View
        style={StyleSheet.flatten([
          styles.iconContainer,
          iconContainerStyle,
          {
            transform: [
              {
                rotateZ: animation.interpolate({
                  inputRange: [0, 1],
                  outputRange: [iconStartRotation, iconEndRotation],
                }),
              },
            ],
          },
        ])}>
        {icon || <Icon name="chevron-down" />}
      </Animated.View>
    ),
    [animation, icon, iconContainerStyle, iconStartRotation, iconEndRotation],
  );

  const handleRenderTitle = useMemo(() => {
    const title = selection
      ? titleExtractor
        ? titleExtractor(selection.item, selection.index)
        : keyExtractor(selection.item, selection.index)
      : placeholder;

    return (
      <View
        style={StyleSheet.flatten([
          styles.titleContainer,
          titleContainerStyle,
          styles.fixedTitleContainer,
        ])}>
        <Text
          style={StyleSheet.flatten([
            styles.title,
            titleStyle,
            selection
              ? selectedTitleStyle
              : StyleSheet.flatten<TextStyle>([
                  styles.placeholder,
                  placeholderStyle,
                ]),
          ])}>
          {title}
        </Text>
      </View>
    );
  }, [
    selection,
    titleStyle,
    placeholder,
    placeholderStyle,
    selectedTitleStyle,
    titleExtractor,
    keyExtractor,
  ]);

  const handleRenderButton = useMemo(
    () => (
      <Touchable
        testID="button"
        touchableType="normal"
        refView={handleRefButton}
        style={StyleSheet.flatten([
          styles.container,
          containerStyle,
          toggle
            ? StyleSheet.flatten([
                styles.selectedContainer,
                selectedContainerStyle,
              ])
            : {},
          styles.fixedContainer,
        ])}
        onPress={handlePressButton}>
        {handleRenderIcon}
        <View style={fullWidth && styles.sectionTitle}>
          {handleRenderTitle}
        </View>
      </Touchable>
    ),
    [
      toggle,
      animation,
      handleRenderTitle,
      fullWidth,
      containerStyle,
      selectedContainerStyle,
      iconContainerStyle,
      iconStartRotation,
      iconEndRotation,
      icon,
      handleRenderIcon,
      handleRefButton,
    ],
  );

  const handleRenderMenu = useMemo(
    () =>
      layout.current && (
        <Modal
          transparent
          visible={toggle}
          onPressBackdrop={() => setToggle(!toggle)}>
          <Animated.View
            style={StyleSheet.flatten([
              styles.listContainer,
              listContainerStyle,
              styles.fixedListContainer,
              {
                width: layout.current.width,
                left: layout.current.pageX,
                top: layout.current.pageY + layout.current.height,
                opacity: animation.interpolate({
                  inputRange: [0, 1],
                  outputRange: [0, 1],
                }),
              },
            ])}>
            <FlatList
              {...props}
              data={data}
              keyExtractor={keyExtractor}
              renderItem={handleRenderMenuItem}
            />
          </Animated.View>
        </Modal>
      ),
    [
      layout.current,
      toggle,
      props,
      data,
      listContainerStyle,
      keyExtractor,
      handleRenderMenuItem,
    ],
  );

  useDidUpdate(() => setSelection(getInfoFromKey(selected)), [selected]);

  useDidUpdate(handleRunAnimation, [handleRunAnimation]);

  return (
    <>
      {handleRenderButton}
      {handleRenderMenu}
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    borderWidth: 1,
    borderRadius: 4,
    borderColor: 'lightgray',
    backgroundColor: 'whitesmoke',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  fixedContainer: {
    flexDirection: 'row-reverse',
  },
  selectedContainer: {
    borderColor: 'dodgerblue',
    backgroundColor: 'white',
  },
  listContainer: {
    borderWidth: 1,
    borderRadius: 4,
    borderColor: 'lightgray',
    backgroundColor: '#fff',
    maxHeight: 300,
  },
  fixedListContainer: {
    position: 'absolute',
    zIndex: 1,
  },
  listItemContainer: {
    paddingHorizontal: 12,
    paddingTop: 12,
    paddingBottom: 8,
    justifyContent: 'center',
  },
  listItemSelectedContainer: {
    backgroundColor: 'whitesmoke',
  },
  titleContainer: {
    paddingLeft: 12,
  },
  fixedTitleContainer: {
    flex: 1,
  },
  iconContainer: {
    padding: 12,
  },
  sectionTitle: {
    flex: 1,
  },
  title: {
    flex: 1,
    fontSize: 15,
    textAlignVertical: 'center',
  },
  placeholder: {
    color: 'darkgray',
  },
});

Testing:

import React from 'react';
import {fireEvent, render} from '@testing-library/react-native';
import {ObjectPartial} from '../../types';
import Picker, {PickerProps} from './Picker';

const defaultProps: PickerProps<string> = {
  data: ['a', 'b', 'c', 'd'],
  keyExtractor: item => item,
  onSelect: jest.fn(),
  renderItem: () => null,
};

function runtest(name: string, props?: ObjectPartial<PickerProps<string>>) {
  test(name, async () => {
    const {getByTestId} = render(<Picker {...defaultProps} {...props} />);
    const button = getByTestId('button');

    fireEvent.press(button);
  });
}

describe('Picker', () => {
  runtest('default');
  runtest('selected', {selected: 'a'});
  runtest('selected invalid', {selected: 'invalid'});
  runtest('selected titleExtractor', {
    selected: 'a',
    titleExtractor: item => item,
  });
});

Link Project for Testing

Ponderous answered 19/10, 2020 at 1:54 Comment(7)
Please include the component.Anagram
I already put the component and testing, I also provide the link if you want to check it. and thank youPonderous
Does this answer your question? React - Jest - Enzyme: How to mock ref propertiesAnagram
I already tried it but not worked, maybe I'm wrong in implementing it.Ponderous
from what your link do, and error that I got it only works on custom component. but not work on native componentPonderous
do you know other alternative @UKS?Ponderous
hi, did you found any solution for the same?Mendelevium
B
2

Because ref exists, you can patch it in your test like so:

const view = screen.queryByTestId('TestingComponent');

const measure = jest.fn().mockImplementation(f => { f(1, 2 ,3, 4, 5, 6) });

view.parent.instance.measure = measure;

then call fireEvent.

Now measure's callback executes with testing values.

Basting answered 15/3, 2023 at 0:25 Comment(0)
P
0

Import the component and overwrite the property under 'proptype' as follows:

import { View } from 'react-native';
const measureInWindow = jest.fn().mockImplementation(f => { f(1, 2 ,3, 4, 5, 6) });
View.prototype.measureInWindow = measureInWindow;

You can either mock it globally at the top of the test file, or inside of each test statement.

Paquin answered 6/5 at 22:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.