Skip to content

[Bug]: useBottomSheetScrollableCreator is not a function on Jest mock #2580

@renanmav

Description

@renanmav

Version

v5

Reanimated Version

v3

Gesture Handler Version

v2

Platforms

Web

What happened?

I am trying to write a unit test with Jest for component that has @legendapp/list + @gorhom/bottom-sheet.

I am mocking it like so: jest.mock('@gorhom/bottom-sheet', () => require('@gorhom/bottom-sheet/mock'));

I get an error TypeError: (0 , _bottomSheet.useBottomSheetScrollableCreator) is not a function when running the unit test.

Reproduction steps

Use useBottomSheetScrollableCreator in the source code of a component, like so:

LSelect.tsx

import {
  ReactElement,
  ReactNode,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import {Pressable, StyleSheet, TextInput, View} from 'react-native';
import {
  BottomSheetBackdrop,
  BottomSheetModal,
  useBottomSheetScrollableCreator,
} from '@gorhom/bottom-sheet';
import {LegendList} from '@legendapp/list';
import * as Haptics from 'expo-haptics';
import {CaretDownIcon, CaretUpIcon} from 'phosphor-react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {colors} from '@/constants/colors';
import {BottomSheetHandle} from './LBottomSheet';
import {LText} from './LText';

type Props<ItemT> = {
  ref?: React.RefObject<TextInput | null>;
  onChange?: (item: ItemT | null) => void;
  data: readonly ItemT[];
  placeholder?: string;
  value?: string;
  defaultValue?: string;
  keyExtractor: (item: ItemT) => string;
  getLabel: (item: ItemT) => string;
  renderItem: (info: {
    item: ItemT;
    index: number;
    isSelected: boolean;
  }) => ReactNode;
  getIcon?: (item: ItemT) => ReactElement | null;
  enableDynamicSizing?: boolean;
  title?: string;
  testID?: string;
};

export function LSelect<ItemT>({
  ref,
  data,
  onChange,
  placeholder,
  value,
  defaultValue,
  keyExtractor,
  getLabel,
  renderItem,
  getIcon,
  enableDynamicSizing = false,
  title,
  testID,
}: Props<ItemT>) {
  const isControlled = value !== undefined;
  const insets = useSafeAreaInsets();

  const [internalSelected, setInternalSelected] = useState<ItemT | null>(() => {
    if (defaultValue) {
      return data.find(d => keyExtractor(d) === defaultValue) || null;
    }
    return null;
  });

  const selected = isControlled
    ? data.find(d => keyExtractor(d) === value) || null
    : internalSelected;

  const bottomSheetRef = useRef<BottomSheetModal>(null);
  const [bottomSheetVisible, setBottomSheetVisible] = useState(false);

  const handlePress = () => {
    console.log('LSelect > handlePress');
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    setBottomSheetVisible(true);
    bottomSheetRef.current?.present();
  };

  const handleItemPress = (item: ItemT) => {
    if (!isControlled) {
      setInternalSelected(item);
    }
    onChange?.(item);
    bottomSheetRef.current?.dismiss();
  };

  const handleDismiss = () => {
    setBottomSheetVisible(false);
    bottomSheetRef.current?.dismiss();
  };

  const renderBackdrop = (backdropProps: any) => (
    <BottomSheetBackdrop
      {...backdropProps}
      disappearsOnIndex={-1}
      appearsOnIndex={0}
    />
  );

  const wrappedRenderItem = ({item, index}: {item: ItemT; index: number}) => {
    const isSelected = Boolean(
      selected && keyExtractor(item) === keyExtractor(selected),
    );
    const content = renderItem({item, index, isSelected});
    return (
      <Pressable onPress={() => handleItemPress(item)}>{content}</Pressable>
    );
  };

  const BottomSheetLegendListScrollable = useBottomSheetScrollableCreator();

  // @ts-expect-error partial implementation
  useImperativeHandle(ref, () => ({
    focus: () => handlePress(),
    blur: () => handleDismiss(),
  }));

  console.log('LSelect > bottomSheetVisible', bottomSheetVisible);
  console.log('LSelect > data', data);

  return (
    <>
      <Pressable style={styles.container} onPress={handlePress} testID={testID}>
        <View style={styles.triggerContent}>
          {selected && getIcon && (
            <View style={styles.iconContainer}>{getIcon(selected)}</View>
          )}
          <LText style={styles.triggerText}>
            {selected ? getLabel(selected) : placeholder}
          </LText>
        </View>
        {bottomSheetVisible ? <CaretUpIcon /> : <CaretDownIcon />}
      </Pressable>
      <BottomSheetModal
        handleComponent={() => (
          <BottomSheetHandle header={title ? {title} : undefined} />
        )}
        enableDynamicSizing={enableDynamicSizing}
        onDismiss={handleDismiss}
        ref={bottomSheetRef}
        topInset={insets.top}
        snapPoints={['50%', '75%']}
        backdropComponent={renderBackdrop}>
        <LText>Hello</LText> {/* TODO: remove this */}
        <LegendList
          data={data}
          renderItem={wrappedRenderItem}
          keyExtractor={keyExtractor}
          indicatorStyle="black"
          ItemSeparatorComponent={() => <View style={styles.separator} />}
          contentContainerStyle={[
            styles.contentContainer,
            enableDynamicSizing ? {paddingBottom: insets.bottom + 16} : null,
          ]}
          maintainVisibleContentPosition
          renderScrollComponent={BottomSheetLegendListScrollable}
          testID="lselect-legend-list"
        />
      </BottomSheetModal>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    borderWidth: 1,
    borderColor: colors.black_15,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    borderRadius: 8,
    paddingHorizontal: 16,
    height: 56,
    gap: 8,
  },
  triggerContent: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
  },
  contentContainer: {
    paddingHorizontal: 8,
    paddingTop: 8,
  },
  iconContainer: {
    flexShrink: 0,
  },
  triggerText: {
    flex: 1,
  },
  separator: {
    height: 8,
  },
});

Reproduction sample

https://snack.expo.dev

Relevant log output

TypeError: (0 , _bottomSheet.useBottomSheetScrollableCreator) is not a function

      110 |   };
      111 |
    > 112 |   const BottomSheetLegendListScrollable = useBottomSheetScrollableCreator();
          |                                                                          ^
      113 |
      114 |   // @ts-expect-error partial implementation
      115 |   useImperativeHandle(ref, () => ({

      at LSelect (src/components/ui/LSelect.tsx:112:74)
      at Object.react-stack-bottom-frame (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13976:20)
      at renderWithHooks (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:2866:22)
      at updateFunctionComponent (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:6006:19)
      at beginWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7474:18)
      at runWithFiberInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:1574:13)
      at performUnitOfWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10972:22)
      at workLoopSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10816:41)
      at renderRootSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10797:11)
      at performWorkOnRoot (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10384:39)
      at performWorkOnRootViaSchedulerTask (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:2094:7)
      at flushActQueue (node_modules/react/cjs/react.development.js:566:34)
      at node_modules/react/cjs/react.development.js:822:21

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions