veza/apps/web/src/hooks/useIntersectionObserver.test.ts

154 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useRef } from 'react';
import { useIntersectionObserver } from './useIntersectionObserver';
// Mock IntersectionObserver
class MockIntersectionObserver {
observe = vi.fn();
disconnect = vi.fn();
unobserve = vi.fn();
constructor(
public callback: IntersectionObserverCallback,
public options?: IntersectionObserverInit,
) {}
}
describe('useIntersectionObserver', () => {
let mockObserver: MockIntersectionObserver;
beforeEach(() => {
mockObserver = new MockIntersectionObserver(() => {});
global.IntersectionObserver = MockIntersectionObserver as any;
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should return undefined when element ref is null', () => {
const elementRef = { current: null };
const { result } = renderHook(() =>
useIntersectionObserver(elementRef as React.RefObject<Element>, {}),
);
// Initially undefined, and stays undefined when ref is null
expect(result.current).toBeUndefined();
expect(mockObserver.observe).not.toHaveBeenCalled();
});
it('should observe element when ref is provided', () => {
const element = document.createElement('div');
const elementRef = { current: element };
renderHook(() =>
useIntersectionObserver(elementRef as React.RefObject<Element>, {}),
);
// Wait for effect to run
expect(mockObserver.observe).toHaveBeenCalled();
});
it('should disconnect observer on unmount', () => {
const element = document.createElement('div');
const elementRef = { current: element };
const { unmount } = renderHook(() =>
useIntersectionObserver(elementRef as React.RefObject<Element>, {}),
);
unmount();
expect(mockObserver.disconnect).toHaveBeenCalled();
});
it('should use default options', () => {
const element = document.createElement('div');
const elementRef = { current: element };
renderHook(() =>
useIntersectionObserver(elementRef as React.RefObject<Element>, {}),
);
// Verify IntersectionObserver was called
expect(MockIntersectionObserver).toHaveBeenCalled();
const call = (MockIntersectionObserver as any).mock.calls[0];
expect(call[1]).toMatchObject({
threshold: 0,
root: null,
rootMargin: '0%',
});
});
it('should use custom options', () => {
const element = document.createElement('div');
const elementRef = { current: element };
renderHook(() =>
useIntersectionObserver(elementRef as React.RefObject<Element>, {
threshold: 0.5,
rootMargin: '10px',
}),
);
expect(MockIntersectionObserver).toHaveBeenCalled();
const call = (MockIntersectionObserver as any).mock.calls[0];
expect(call[1]).toMatchObject({
threshold: 0.5,
rootMargin: '10px',
});
});
it('should freeze when freezeOnceVisible is true and element is intersecting', () => {
const element = document.createElement('div');
const elementRef = { current: element };
const { result, rerender } = renderHook(
({ freeze }) =>
useIntersectionObserver(elementRef as React.RefObject<Element>, {
freezeOnceVisible: freeze,
}),
{
initialProps: { freeze: true },
},
);
// Get the callback from the constructor call
const constructorCall = (MockIntersectionObserver as any).mock.calls.find(
(call: any[]) => call[0] && typeof call[0] === 'function',
);
if (constructorCall) {
const callback = constructorCall[0];
const entry = {
isIntersecting: true,
} as IntersectionObserverEntry;
callback([entry], {} as IntersectionObserver);
}
rerender({ freeze: true });
// Should not observe again when frozen
expect(mockObserver.observe).toHaveBeenCalledTimes(1);
});
it('should handle missing IntersectionObserver gracefully', () => {
const originalIO = global.IntersectionObserver;
// @ts-ignore
delete global.IntersectionObserver;
const element = document.createElement('div');
const elementRef = { current: element };
const { result } = renderHook(() =>
useIntersectionObserver(elementRef as React.RefObject<Element>, {}),
);
expect(result.current).toBeUndefined();
global.IntersectionObserver = originalIO;
});
});