diff --git a/src/internal/hooks/use-intersection-observer/__tests__/use-intersection-observer.test.tsx b/src/internal/hooks/use-intersection-observer/__tests__/use-intersection-observer.test.tsx index 85656c2183..2be57c8beb 100644 --- a/src/internal/hooks/use-intersection-observer/__tests__/use-intersection-observer.test.tsx +++ b/src/internal/hooks/use-intersection-observer/__tests__/use-intersection-observer.test.tsx @@ -1,52 +1,111 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import { render } from '@testing-library/react'; +import { act, cleanup, render } from '@testing-library/react'; -import { useIntersectionObserver } from '..'; - -declare global { - interface Window { - IntersectionObserver: any; - } -} +import { useIntersectionObserver } from '../../../../../lib/components/internal/hooks/use-intersection-observer'; function TestComponent({ initialState }: { initialState?: boolean }) { const { ref, isIntersecting } = useIntersectionObserver({ initialState }); return
; } -const mockObserve = jest.fn(); -const mockIntersectionObserver = jest.fn(() => ({ - observe: mockObserve, - disconnect: jest.fn(), -})); +type MockIntersectionObserverCallback = ( + // only fields actually used in our implementation + entries: Array> +) => void; +let mockObserverInstance: (IntersectionObserver & { callback: MockIntersectionObserverCallback }) | undefined; + +class MockIntersectionObserver { + constructor(callback: MockIntersectionObserverCallback) { + if (mockObserverInstance) { + throw new Error('only one observer instance expected per test'); + } + const instance = { + observe: jest.fn() as IntersectionObserver['observe'], + disconnect: jest.fn() as IntersectionObserver['disconnect'], + } as IntersectionObserver; + mockObserverInstance = { + ...instance, + callback, + }; + return instance; + } +} describe('useIntersectionObserver', () => { beforeEach(() => { - window.IntersectionObserver = mockIntersectionObserver; + window.IntersectionObserver = MockIntersectionObserver as unknown as typeof IntersectionObserver; jest.clearAllMocks(); }); - it('should create an observer with the defined target element', async () => { - const { findByTestId } = render(); - const target = await findByTestId('test'); + afterEach(() => { + cleanup(); + expect(mockObserverInstance!.disconnect).toHaveBeenCalled(); + mockObserverInstance = undefined; + }); + + it('should create an observer with the defined target element', () => { + const { getByTestId } = render(); + const target = getByTestId('test'); + + expect(mockObserverInstance!.observe).toHaveBeenCalledWith(target); + }); + + it('defaults to not intersecting', () => { + const { getByTestId } = render(); + const target = getByTestId('test'); + + expect(target.dataset.value).toBe('false'); + }); + + it('allows overriding the default value', () => { + const { getByTestId } = render(); + const target = getByTestId('test'); - expect(mockIntersectionObserver).toHaveBeenCalled(); - expect(mockObserve).toHaveBeenCalledWith(target); + expect(target.dataset.value).toBe('true'); }); - it('defaults to not intersecting', async () => { - const { findByTestId } = render(); - const target = await findByTestId('test'); + it('updates value with a callback', () => { + const { getByTestId } = render(); + + const target = getByTestId('test'); + expect(target.dataset.value).toBe('false'); + + act(() => mockObserverInstance!.callback([{ time: 0, isIntersecting: true }])); + expect(target.dataset.value).toBe('true'); + act(() => mockObserverInstance!.callback([{ time: 0, isIntersecting: false }])); expect(target.dataset.value).toBe('false'); }); - it('allows overriding the default value', async () => { - const { findByTestId } = render(); - const target = await findByTestId('test'); + it('should resolve the final state if multiple observations occurred', () => { + const { getByTestId } = render(); + + act(() => + mockObserverInstance!.callback([ + { time: 0, isIntersecting: true }, + { time: 1, isIntersecting: false }, + { time: 2, isIntersecting: true }, + ]) + ); + + const target = getByTestId('test'); + expect(target.dataset.value).toBe('true'); + }); + + it('should sort observations by time', () => { + const { getByTestId } = render(); + + act(() => + mockObserverInstance!.callback([ + { time: 1, isIntersecting: false }, + { time: 2, isIntersecting: true }, + { time: 0, isIntersecting: false }, + ]) + ); + const target = getByTestId('test'); expect(target.dataset.value).toBe('true'); }); }); diff --git a/src/internal/hooks/use-intersection-observer/index.ts b/src/internal/hooks/use-intersection-observer/index.ts index ed4786871d..abe6d9de2f 100644 --- a/src/internal/hooks/use-intersection-observer/index.ts +++ b/src/internal/hooks/use-intersection-observer/index.ts @@ -41,7 +41,15 @@ export function useIntersectionObserver({ } catch { // Tried to access a cross-origin iframe. Fall back to current IntersectionObserver. } - observerRef.current = new TopLevelIntersectionObserver(([entry]) => setIsIntersecting(entry.isIntersecting)); + observerRef.current = new TopLevelIntersectionObserver(entries => { + let latestEntry = entries[0]; + for (const entry of entries) { + if (entry.time > latestEntry.time) { + latestEntry = entry; + } + } + setIsIntersecting(latestEntry.isIntersecting); + }); observerRef.current.observe(targetElement); } }, []);