Files
TREK/client/src/hooks/useElementSize.test.ts
T

106 lines
3.8 KiB
TypeScript

// FE-HOOK-ELSIZE-001 to FE-HOOK-ELSIZE-006
import { act, renderHook } from '@testing-library/react';
import { useElementSize } from './useElementSize';
// tests/setup.ts installs a no-op ResizeObserver. Swap in one that hands the
// callback back so a resize can be simulated, and count the connections so the
// disconnect-on-detach contract is observable.
const observers: { el: Element; fire: () => void; disconnected: boolean }[] = [];
class ControllableResizeObserver {
private entry: { el: Element; fire: () => void; disconnected: boolean } | null = null;
constructor(private cb: ResizeObserverCallback) {}
observe(el: Element) {
this.entry = { el, fire: () => this.cb([], this as unknown as ResizeObserver), disconnected: false };
observers.push(this.entry);
}
unobserve() {}
disconnect() {
if (this.entry) this.entry.disconnected = true;
}
}
function element(width: number, height: number): HTMLElement {
const el = document.createElement('div');
Object.defineProperty(el, 'offsetWidth', { value: width, configurable: true });
Object.defineProperty(el, 'offsetHeight', { value: height, configurable: true });
return el;
}
function resize(el: HTMLElement, width: number, height: number): void {
Object.defineProperty(el, 'offsetWidth', { value: width, configurable: true });
Object.defineProperty(el, 'offsetHeight', { value: height, configurable: true });
}
beforeEach(() => {
observers.length = 0;
vi.stubGlobal('ResizeObserver', ControllableResizeObserver);
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe('useElementSize', () => {
it('FE-HOOK-ELSIZE-001: starts at zero before an element is attached', () => {
const { result } = renderHook(() => useElementSize());
expect(result.current.width).toBe(0);
expect(result.current.height).toBe(0);
expect(observers).toHaveLength(0);
});
it('FE-HOOK-ELSIZE-002: measures the element as soon as the ref is attached', () => {
const { result } = renderHook(() => useElementSize());
act(() => result.current.ref(element(320, 180)));
expect(result.current.width).toBe(320);
expect(result.current.height).toBe(180);
expect(observers).toHaveLength(1);
});
it('FE-HOOK-ELSIZE-003: picks up a later resize of the same element', () => {
const { result } = renderHook(() => useElementSize());
const el = element(320, 180);
act(() => result.current.ref(el));
resize(el, 640, 400);
act(() => observers[0].fire());
expect(result.current.width).toBe(640);
expect(result.current.height).toBe(400);
});
it('FE-HOOK-ELSIZE-004: keeps the same state object when the size is unchanged', () => {
const { result } = renderHook(() => useElementSize());
act(() => result.current.ref(element(200, 100)));
const before = result.current;
act(() => observers[0].fire());
// No re-render — the hook bailed out of setState because nothing moved.
expect(result.current.width).toBe(before.width);
expect(result.current.height).toBe(before.height);
});
it('FE-HOOK-ELSIZE-005: disconnects the old observer when the ref moves to another element', () => {
const { result } = renderHook(() => useElementSize());
act(() => result.current.ref(element(100, 50)));
act(() => result.current.ref(element(300, 150)));
expect(observers).toHaveLength(2);
expect(observers[0].disconnected).toBe(true);
expect(observers[1].disconnected).toBe(false);
expect(result.current.width).toBe(300);
});
it('FE-HOOK-ELSIZE-006: disconnects when React detaches the ref with null', () => {
const { result } = renderHook(() => useElementSize());
act(() => result.current.ref(element(100, 50)));
act(() => result.current.ref(null));
expect(observers[0].disconnected).toBe(true);
// The last measured size is kept — only the observation stops.
expect(result.current.width).toBe(100);
});
});