import { act, render } from '@testing-library/react-native'; import * as React from 'react'; import { KeyboardAvoidingView, Text } from 'react-native'; import * as UseBottomSheetGestureHandler from '../hooks/useBottomSheetGestureHandler'; import * as UseTimedAction from '../hooks/useTimedAction'; import * as UseChecks from '../internal/useChecks'; import { BottomSheet } from './BottomSheet'; beforeEach(() => { jest.clearAllMocks(); jest .spyOn(UseBottomSheetGestureHandler, 'useBottomSheetGestureHandler') .mockReturnValue({ gestureHandler: jest.fn() }); }); jest.useFakeTimers(); describe('BottomSheet', () => { describe('When __DEV__ is true', () => { it('perform a checks on closeActionAccessibilityLabel', () => { const noUndefinedProperty = jest.fn(); const accessibilityLabelChecker = jest.fn(); // @ts-ignore jest.spyOn(UseChecks, 'useChecks').mockReturnValue({ noUndefinedProperty, noUppercaseStringChecker: accessibilityLabelChecker, } as any); render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} />, ); expect(noUndefinedProperty).toHaveBeenCalledWith({ properties: { closeActionAccessibilityLabel: 'close me' }, property: 'closeActionAccessibilityLabel', rule: 'BOTTOM_SHEET_CLOSE_ACTION', }); expect(accessibilityLabelChecker).toHaveBeenCalledWith({ text: 'close me', }); }); it('applies the style from useChecks', () => { const noUndefinedProperty = jest.fn(); const accessibilityLabelChecker = jest.fn(); // @ts-ignore jest.spyOn(UseChecks, 'useChecks').mockReturnValue({ debugStyle: { backgroundColor: 'cyan' }, noUndefinedProperty, noUppercaseStringChecker: accessibilityLabelChecker, } as any); const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} testID="bottom-sheet" />, ); expect(getByTestId('bottom-sheet-overlay-wrapper').props.style).toEqual({ backgroundColor: 'cyan', flex: 1, opacity: 0, }); }); }); describe('When __DEV__ is false', () => { beforeEach(() => { // @ts-ignore global.__DEV__ = false; }); it('perform a checks on closeActionAccessibilityLabel', () => { const { BottomSheet: OriginalBottomSheet } = require('./BottomSheet'); const noUndefinedProperty = jest.fn(); const accessibilityLabelChecker = jest.fn(); // @ts-ignore jest.spyOn(UseChecks, 'useChecks').mockReturnValue({ noUndefinedProperty, noUppercaseStringChecker: accessibilityLabelChecker, } as any); render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} />, ); expect(noUndefinedProperty).not.toHaveBeenCalledWith(); expect(accessibilityLabelChecker).not.toHaveBeenCalledWith(); }); it('does not apply the style from useChecks', () => { const noUndefinedProperty = jest.fn(); const accessibilityLabelChecker = jest.fn(); // @ts-ignore jest.spyOn(UseChecks, 'useChecks').mockReturnValue({ debugStyle: { backgroundColor: 'cyan' }, noUndefinedProperty, noUppercaseStringChecker: accessibilityLabelChecker, } as any); const { BottomSheet: OriginalBottomSheet } = require('./BottomSheet'); const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} testID="bottom-sheet" />, ); expect(getByTestId('bottom-sheet-overlay-wrapper').props.style).toEqual({ backgroundColor: 'rgba(0, 0, 0, 0.5)', flex: 1, opacity: 0, }); }); }); it('does not render the content when not visible', () => { const { toJSON } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} testID="bottom-sheet" />, ); expect(toJSON()).toMatchInlineSnapshot(` `); }); describe('Styling...', () => { it('allows customising the overlay style', () => { const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} overlayStyle={{ backgroundColor: 'yellow', width: 42 }} testID="bottom-sheet" />, ); expect(getByTestId('bottom-sheet-overlay-wrapper').props.style).toEqual({ backgroundColor: 'yellow', flex: 1, opacity: 0, width: 42, }); }); it('allows customising the bottom sheet panel', () => { const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} bottomSheetStyle={{ borderRadius: 42 }} testID="bottom-sheet" />, ); expect(getByTestId('bottom-sheet-panel').props.style).toEqual({ backgroundColor: '#fff', borderRadius: 42, flex: 1, width: '100%', }); }); it('allows customising the line style', () => { const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} handleStyle={{ width: '100%' }} testID="bottom-sheet" />, ); expect(getByTestId('bottom-sheet-line').props.style).toEqual([ { alignSelf: 'center', backgroundColor: 'grey', borderRadius: 2, height: 4, marginBottom: 24, marginTop: 12, width: 48, }, { width: '100%' }, ]); }); it('allows not rendering the line', () => { const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} handleComponent="none" testID="bottom-sheet" />, ); expect(() => getByTestId('bottom-sheet-line')).toThrow(); }); it('allows customising the ScrollView style', () => { const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} scrollViewProps={{ style: { backgroundColor: 'fucsia', }, }} topInset={3} testID="bottom-sheet" />, ); expect(getByTestId('bottom-sheet-scrollview').props.style).toEqual([ { maxHeight: 1201 }, { backgroundColor: 'fucsia' }, ]); }); it('allows rendering a custom header', () => { const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} />, ); expect(getByTestId('Header')).toBeDefined(); }); }); // TODO: Fix it.skip('on close it waits for the animation to be completed before hiding the modal', () => { const { rerender, getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} animationDuration={100} headerComponent={Header} />, ); act(() => rerender( {}} closeActionAccessibilityLabel={'close me'} headerComponent={Header} animationDuration={100} testID="bottom-sheet" />, ), ); expect(getByTestId('bottom-sheet').props.visible).toBe(true); jest.advanceTimersByTime(99); expect(getByTestId('bottom-sheet').props.visible).toBe(true); act(() => { jest.advanceTimersByTime(2); }); expect(getByTestId('bottom-sheet').props.visible).toBe(false); }); it('uses the animatedStyle for the bottom sheet wrapper', () => { const { getByTestId } = render( {}} closeActionAccessibilityLabel={'close me'} animationDuration={100} headerComponent={Header} testID="bottom-sheet" />, ); expect(getByTestId('bottom-sheet-wrapper').props.style).toEqual({ alignSelf: 'flex-end', bottom: 0, flex: 1, flexDirection: 'column', position: 'absolute', width: '100%', maxHeight: 1200.6000000000001, transform: [{ translateY: 0 }], }); }); // TODO: Fix it.skip('passes the layout height into to the useBottomSheetGestureHandler hook', () => { const useBottomSheetGestureHandler = jest .spyOn(UseBottomSheetGestureHandler, 'useBottomSheetGestureHandler') .mockReturnValue({ gestureHandler: jest.fn() }); const onClose = jest.fn(); const { getByTestId } = render( Header} testID="bottom-sheet" />, ); act(() => { getByTestId('bottom-sheet-panel').props.onLayout({ nativeEvent: { layout: { height: 42 } }, }); }); expect(useBottomSheetGestureHandler).toHaveBeenCalledWith( expect.objectContaining({ closeDistance: 0.1, contentHeight: { value: 42 }, onClose: onClose, translateY: { value: 0 }, dragOpacity: { value: 0 }, minVelocityToClose: 1000, overlayOpacity: 1, }), ); }); it('uses the onTimeout function from the useTimedAction hook to auto-close the bottom sheet', () => { const onTimeout = jest.fn(); jest.spyOn(UseTimedAction, 'useTimedAction').mockReturnValue({ onTimeout, }); render( {}} closeActionAccessibilityLabel={'close me'} animationDuration={100} headerComponent={Header} testID="bottom-sheet" />, ); expect(onTimeout).toHaveBeenCalledWith(expect.any(Function), 100); }); it('wraps the Modal content inside KeyboardAvoidingView when the avoidKeyboard prop is true and shouldHandleKeyboardEvents = false', () => { const { UNSAFE_getByType } = render( {}} closeActionAccessibilityLabel={'close me'} animationDuration={100} headerComponent={Header} testID="bottom-sheet" avoidKeyboard={true} shouldHandleKeyboardEvents={false} />, ); expect(UNSAFE_getByType(KeyboardAvoidingView)).toBeDefined(); }); it('does not wrap the Modal content inside KeyboardAvoidingView when the avoidKeyboard prop is true and shouldHandleKeyboardEvents = true', () => { const { UNSAFE_getByType } = render( {}} closeActionAccessibilityLabel={'close me'} animationDuration={100} headerComponent={Header} testID="bottom-sheet" avoidKeyboard={true} />, ); expect(() => UNSAFE_getByType(KeyboardAvoidingView)).toThrow(); }); it.each([undefined, false])( 'does not wraps the Modal content inside KeyboardAvoidingView when avoidKeyboard prop is %s', avoidKeyboard => { const { UNSAFE_getByType } = render( {}} closeActionAccessibilityLabel={'close me'} animationDuration={100} headerComponent={Header} testID="bottom-sheet" avoidKeyboard={avoidKeyboard} />, ); expect(() => UNSAFE_getByType(KeyboardAvoidingView)).toThrow(); }, ); it('calls onClose correctly', () => { const onClose = jest.fn(); const { getByTestId } = render( Header} testID="bottom-sheet" />, ); act(() => { const event = { some: 'props' }; getByTestId('bottom-sheet').props.onRequestClose(event); }); expect(onClose).toHaveBeenCalledWith(); }); it.todo('calculate correct max height'); it.todo('calculate correct scrollview height'); it.todo('handles keyboard correctly'); }); jest.mock('../hooks/useBottomSheetGestureHandler'); jest.mock('../internal/useChecks');