import React from 'react'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { act, fireEvent, render } from '@testing-library/react'; import { PixelMouseParallax } from '../../parallax/PixelMouseParallax'; describe('PixelMouseParallax', () => { let rafCallbacks: FrameRequestCallback[]; beforeEach(() => { rafCallbacks = []; vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { rafCallbacks.push(cb); return rafCallbacks.length; }); vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {}); // jsdom rects are all zeros — give the fallback parent (document.body) // a real box so cursor normalisation produces finite values. vi.spyOn(document.body, 'getBoundingClientRect').mockReturnValue({ left: 0, top: 0, right: 200, bottom: 100, width: 200, height: 100, x: 0, y: 0, toJSON: () => ({}), } as DOMRect); }); afterEach(() => { vi.restoreAllMocks(); }); /** Run the most recently queued animation frame. */ function runFrame() { const cb = rafCallbacks[rafCallbacks.length - 1]; act(() => { cb(performance.now()); }); } function parseTranslate(el: HTMLElement): { x: number; y: number } { const m = /translate3d\((-?[\d.]+)px, (-?[\d.]+)px, 0\)/.exec(el.style.transform); expect(m, `transform should be a translate3d: "${el.style.transform}"`).toBeTruthy(); return { x: Number(m![1]), y: Number(m![2]) }; } it('renders children with the will-change-transform class', () => { const { container, getByText } = render( follow me , ); expect(getByText('follow me')).toBeTruthy(); const el = container.firstElementChild as HTMLElement; expect(el.className).toContain('will-change-transform'); }); it('moves towards the cursor after a mousemove (lerped per frame)', () => { const { container } = render(x); const el = container.firstElementChild as HTMLElement; // Bottom-right corner of the 200x100 parent box → normalized (1, 1). fireEvent.mouseMove(window, { clientX: 200, clientY: 100 }); runFrame(); const { x, y } = parseTranslate(el); // First lerp step: 0 + (20 - 0) * 0.08 = 1.6 towards the target. expect(x).toBeCloseTo(1.6, 5); expect(y).toBeCloseTo(1.6, 5); }); it('keeps approaching the target on subsequent frames', () => { const { container } = render(x); const el = container.firstElementChild as HTMLElement; fireEvent.mouseMove(window, { clientX: 200, clientY: 100 }); runFrame(); const first = parseTranslate(el).x; runFrame(); const second = parseTranslate(el).x; expect(second).toBeGreaterThan(first); expect(second).toBeLessThanOrEqual(20); }); it('invert moves away from the cursor instead', () => { const { container } = render( x, ); const el = container.firstElementChild as HTMLElement; fireEvent.mouseMove(window, { clientX: 200, clientY: 100 }); runFrame(); const { x, y } = parseTranslate(el); expect(x).toBeLessThan(0); expect(y).toBeLessThan(0); }); it('removes the mousemove listener and cancels the loop on unmount', () => { const removeSpy = vi.spyOn(window, 'removeEventListener'); const { unmount } = render(x); unmount(); const removed = removeSpy.mock.calls.map((c) => c[0]); expect(removed).toContain('mousemove'); expect(window.cancelAnimationFrame).toHaveBeenCalled(); }); });