Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <div ref={ref} data-testid="test" data-value={isIntersecting} />;
}

const mockObserve = jest.fn();
const mockIntersectionObserver = jest.fn(() => ({
observe: mockObserve,
disconnect: jest.fn(),
}));
type MockIntersectionObserverCallback = (
// only fields actually used in our implementation
entries: Array<Pick<IntersectionObserverEntry, 'isIntersecting' | 'time'>>
) => 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(<TestComponent />);
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(<TestComponent />);
const target = getByTestId('test');

expect(mockObserverInstance!.observe).toHaveBeenCalledWith(target);
});

it('defaults to not intersecting', () => {
const { getByTestId } = render(<TestComponent />);
const target = getByTestId('test');

expect(target.dataset.value).toBe('false');
});

it('allows overriding the default value', () => {
const { getByTestId } = render(<TestComponent initialState={true} />);
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(<TestComponent />);
const target = await findByTestId('test');
it('updates value with a callback', () => {
const { getByTestId } = render(<TestComponent />);

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(<TestComponent initialState={true} />);
const target = await findByTestId('test');
it('should resolve the final state if multiple observations occurred', () => {
const { getByTestId } = render(<TestComponent />);

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(<TestComponent />);

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');
});
});
10 changes: 9 additions & 1 deletion src/internal/hooks/use-intersection-observer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,15 @@ export function useIntersectionObserver<T extends HTMLElement>({
} 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);
}
}, []);
Expand Down
Loading