import React from 'react'; import { Text } from 'react-native'; import { act, cleanup, render, renderHook, screen } from '@testing-library/react-native'; import { clearClosingPortalLayout, closeOverlay, finalizeCloseOverlay, openOverlay, overlayStore, setClosingPortalLayout, useClosingPortalHostBlacklist, useClosingPortalHostBlacklistState, useShouldTeleportToClosingPortal, } from '../message-overlay-store'; const BASE_RECT = { h: 40, w: 100, x: 10, y: 20 }; type RegisteredLayout = { hostName: string; id: string; }; const flushAnimationFrameQueue = () => { act(() => { jest.runAllTimers(); }); }; const BlacklistRegistrar = ({ enabled = true, hostNames, }: { enabled?: boolean; hostNames: string[]; }) => { useClosingPortalHostBlacklist(hostNames, enabled); return null; }; const BlacklistProbe = () => { const hostNames = useClosingPortalHostBlacklistState(); return {hostNames.join(',') || 'empty'}; }; describe('message overlay store portal hooks', () => { let registeredLayouts: RegisteredLayout[] = []; const rememberLayout = (hostName: string, id: string, layout = BASE_RECT) => { registeredLayouts.push({ hostName, id }); act(() => { setClosingPortalLayout(hostName, id, layout); }); }; beforeEach(() => { jest.useFakeTimers(); registeredLayouts = []; act(() => { finalizeCloseOverlay(); overlayStore.next({ closing: false, closingPortalHostBlacklist: [], id: undefined, messageId: undefined, }); }); }); afterEach(() => { cleanup(); act(() => { registeredLayouts.forEach(({ hostName, id }) => { clearClosingPortalLayout(hostName, id); }); finalizeCloseOverlay(); overlayStore.next({ closing: false, closingPortalHostBlacklist: [], id: undefined, messageId: undefined, }); }); registeredLayouts = []; jest.clearAllTimers(); jest.useRealTimers(); }); it('returns true only for the top-most registration when the overlay is closing', () => { const first = renderHook(() => useShouldTeleportToClosingPortal('overlay-composer', 'first')); const second = renderHook(() => useShouldTeleportToClosingPortal('overlay-composer', 'second')); rememberLayout('overlay-composer', 'first'); expect(first.result.current).toBe(false); expect(second.result.current).toBe(false); act(() => { openOverlay('message-1'); closeOverlay(); }); flushAnimationFrameQueue(); expect(first.result.current).toBe(true); expect(second.result.current).toBe(false); rememberLayout('overlay-composer', 'second', { ...BASE_RECT, x: 30, y: 50 }); expect(first.result.current).toBe(false); expect(second.result.current).toBe(true); act(() => { clearClosingPortalLayout('overlay-composer', 'second'); }); expect(first.result.current).toBe(true); expect(second.result.current).toBe(false); first.unmount(); second.unmount(); }); it('does not enter closing when closeOverlay is called without an active overlay id', () => { act(() => { closeOverlay(); }); flushAnimationFrameQueue(); expect(overlayStore.getLatestValue()).toMatchObject({ closing: false, id: undefined, }); }); it('does not teleport when closing is true but there is no active overlay id', () => { const result = renderHook(() => useShouldTeleportToClosingPortal('overlay-composer', 'composer'), ); rememberLayout('overlay-composer', 'composer'); act(() => { overlayStore.next({ closing: true, closingPortalHostBlacklist: [], id: undefined, messageId: undefined, }); }); expect(result.result.current).toBe(false); result.unmount(); }); it('restores the previous blacklist when the top blacklist registration disappears', () => { const { rerender } = render( <> , ); expect(screen.getByTestId('blacklist-state')).toHaveTextContent('overlay-header'); rerender( <> , ); expect(screen.getByTestId('blacklist-state')).toHaveTextContent('overlay-composer'); rerender( <> , ); expect(screen.getByTestId('blacklist-state')).toHaveTextContent('overlay-header'); rerender(); expect(screen.getByTestId('blacklist-state')).toHaveTextContent('empty'); }); it('prevents teleporting blacklisted hosts even when they are top of stack and closing', () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( <> {children} ); const blocked = renderHook( () => useShouldTeleportToClosingPortal('overlay-composer', 'blocked'), { wrapper }, ); const allowed = renderHook( () => useShouldTeleportToClosingPortal('overlay-header', 'allowed'), { wrapper, }, ); rememberLayout('overlay-composer', 'blocked'); rememberLayout('overlay-header', 'allowed', { ...BASE_RECT, x: 90, y: 120 }); act(() => { openOverlay('message-1'); closeOverlay(); }); flushAnimationFrameQueue(); expect(blocked.result.current).toBe(false); expect(allowed.result.current).toBe(true); blocked.unmount(); allowed.unmount(); }); });