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);
}
}, []);