import React from 'react'; import { StyleSheet, Text } from 'react-native'; import * as SafeAreaContext from 'react-native-safe-area-context'; import { act, cleanup, fireEvent, render, screen } from '@testing-library/react-native'; import { finalizeCloseOverlay, openOverlay, overlayStore, setOverlayBottomH, setOverlayMessageH, setOverlayTopH, } from '../../../state-store'; import { WithComponents } from '../../componentsContext/ComponentsContext'; import { MessageActionsProps, MessageOverlayHostLayer } from '../MessageOverlayHostLayer'; jest.mock('react-native', () => { const actual = jest.requireActual('react-native'); return new Proxy(actual, { get(target, prop, receiver) { if (prop === 'useWindowDimensions') { return () => ({ fontScale: 1, height: 200, scale: 1, width: 320 }); } return Reflect.get(target, prop, receiver); }, }); }); jest.mock('react-native-reanimated', () => { const React = require('react'); const actual = jest.requireActual('react-native-reanimated/mock'); const { View } = require('react-native'); const useStableSharedValue = (init: unknown) => { const ref = React.useRef<{ value: unknown; }>(); if (!ref.current) { const value = { value: init }; ref.current = new Proxy(value, { get(target, prop) { if (prop === 'value') { return target.value; } return undefined; }, set(target, prop, nextValue) { if (prop === 'value') { target.value = nextValue; return true; } return false; }, }); } return ref.current; }; return { ...actual, Animated: { ...actual.default, View, }, default: { ...actual.default, View, }, clamp: (value: number, min: number, max: number) => Math.min(Math.max(value, min), max), runOnJS: (fn: (...args: unknown[]) => unknown) => fn, useAnimatedReaction: ( prepare: () => unknown, react: (current: unknown, previous: unknown) => void, ) => { react(prepare(), undefined); }, useAnimatedStyle: (updater: () => unknown) => updater(), useDerivedValue: (updater: () => unknown) => ({ value: updater() }), useSharedValue: useStableSharedValue, withSpring: (value: unknown) => value, }; }); const TOP_RECT = { h: 20, w: 90, x: 5, y: 0 }; const MESSAGE_RECT = { h: 50, w: 180, x: 10, y: 0 }; const BOTTOM_RECT = { h: 30, w: 140, x: 20, y: 100 }; const NoopBackground = () => null; const CustomMessageActions = ({ bottomItemStyle, hostStyle, topItemStyle, }: MessageActionsProps) => ( <> Custom ); const flushAnimationFrameQueue = () => { act(() => { jest.runAllTimers(); }); }; describe('MessageOverlayHostLayer', () => { let useSafeAreaInsetsSpy: jest.SpyInstance; beforeEach(() => { jest.useFakeTimers(); useSafeAreaInsetsSpy = jest.spyOn(SafeAreaContext, 'useSafeAreaInsets').mockReturnValue({ bottom: 15, left: 0, right: 0, top: 10, }); act(() => { finalizeCloseOverlay(); overlayStore.next({ closing: false, closingPortalHostBlacklist: [], id: undefined, messageId: undefined, }); }); }); afterEach(() => { cleanup(); act(() => { finalizeCloseOverlay(); overlayStore.next({ closing: false, closingPortalHostBlacklist: [], id: undefined, messageId: undefined, }); }); useSafeAreaInsetsSpy.mockRestore(); jest.clearAllTimers(); jest.useRealTimers(); }); it('renders the custom background only while active and pressing the backdrop starts closing', () => { const CustomBackground = () => Background; render( , ); expect(screen.queryByTestId('custom-background')).toBeNull(); expect(screen.queryByTestId('message-overlay-backdrop')).toBeNull(); act(() => { openOverlay('message-1'); }); expect(screen.getByTestId('custom-background')).toBeTruthy(); expect(screen.getByTestId('message-overlay-backdrop')).toBeTruthy(); fireEvent.press(screen.getByTestId('message-overlay-backdrop')); flushAnimationFrameQueue(); expect(overlayStore.getLatestValue().closing).toBe(true); }); it('positions and translates the top, message, and bottom hosts using the registered rects', () => { const renderTree = () => ( ); const { rerender } = render(renderTree()); act(() => {}); act(() => { setOverlayTopH(TOP_RECT); setOverlayMessageH(MESSAGE_RECT); setOverlayBottomH(BOTTOM_RECT); openOverlay('message-1'); }); rerender(renderTree()); const topSlot = screen.getByTestId('message-overlay-top'); const messageSlot = screen.getByTestId('message-overlay-message'); const bottomSlot = screen.getByTestId('message-overlay-bottom'); expect(StyleSheet.flatten(topSlot.props.style)).toMatchObject({ height: TOP_RECT.h, left: TOP_RECT.x, position: 'absolute', top: TOP_RECT.y, transform: [{ scale: 1 }, { translateY: 38 }], width: TOP_RECT.w, }); expect(StyleSheet.flatten(messageSlot.props.style)).toMatchObject({ height: MESSAGE_RECT.h, left: MESSAGE_RECT.x, position: 'absolute', top: MESSAGE_RECT.y, transform: [{ translateY: 38 }], width: MESSAGE_RECT.w, }); expect(StyleSheet.flatten(bottomSlot.props.style)).toMatchObject({ height: BOTTOM_RECT.h, left: BOTTOM_RECT.x, position: 'absolute', top: BOTTOM_RECT.y, transform: [{ scale: 1 }, { translateY: -12 }], width: BOTTOM_RECT.w, }); }); it('resets host geometry after finalizeCloseOverlay clears the registered rects', () => { const renderTree = () => ( ); const { rerender } = render(renderTree()); act(() => {}); act(() => { setOverlayTopH(TOP_RECT); setOverlayMessageH(MESSAGE_RECT); setOverlayBottomH(BOTTOM_RECT); openOverlay('message-1'); }); rerender(renderTree()); expect( StyleSheet.flatten(screen.getByTestId('message-overlay-message').props.style), ).toMatchObject({ height: MESSAGE_RECT.h, width: MESSAGE_RECT.w, }); act(() => { finalizeCloseOverlay(); }); rerender(renderTree()); expect(StyleSheet.flatten(screen.getByTestId('message-overlay-top').props.style)).toMatchObject( { height: 0, }, ); expect( StyleSheet.flatten(screen.getByTestId('message-overlay-message').props.style), ).toMatchObject({ height: 0 }); expect( StyleSheet.flatten(screen.getByTestId('message-overlay-bottom').props.style), ).toMatchObject({ height: 0, }); }); it('renders MessageActions override instead of the default host wrappers when provided', () => { const renderTree = () => ( ); const { rerender } = render(renderTree()); act(() => { setOverlayTopH(TOP_RECT); setOverlayMessageH(MESSAGE_RECT); setOverlayBottomH(BOTTOM_RECT); openOverlay('message-1'); }); rerender(renderTree()); expect(screen.getByTestId('custom-message-actions')).toBeTruthy(); expect(screen.queryByTestId('message-overlay-top')).toBeNull(); expect(screen.queryByTestId('message-overlay-message')).toBeNull(); expect(screen.queryByTestId('message-overlay-bottom')).toBeNull(); expect( StyleSheet.flatten(screen.getByTestId('custom-message-actions-top').props.style), ).toMatchObject({ height: TOP_RECT.h, width: TOP_RECT.w, }); expect( StyleSheet.flatten(screen.getByTestId('custom-message-actions-message').props.style), ).toMatchObject({ height: MESSAGE_RECT.h, width: MESSAGE_RECT.w, }); expect( StyleSheet.flatten(screen.getByTestId('custom-message-actions-bottom').props.style), ).toMatchObject({ height: BOTTOM_RECT.h, width: BOTTOM_RECT.w, }); }); });