// @vitest-environment jsdom import { render, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import GradientBlinds from './GradientBlinds'; // Mock ogl vi.mock('ogl', () => { return { Renderer: class { gl = { // Use a real canvas element so appendChild works canvas: document.createElement('canvas'), drawingBufferWidth: 100, drawingBufferHeight: 100, }; setSize() {} render() {} }, Program: class { remove() {} }, Mesh: class { remove() {} }, Triangle: class { remove() {} }, }; }); // Mock ResizeObserver globalThis.ResizeObserver = class ResizeObserver { observe() {} unobserve() {} disconnect() {} }; describe('GradientBlinds', () => { let rafSpy: any; let cancelRafSpy: any; let ioCallback: (entries: IntersectionObserverEntry[]) => void; let observeSpy: any; let disconnectSpy: any; beforeEach(() => { rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((_cb: any) => { return 123; }); cancelRafSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation((_id: number) => { }); // Mock IntersectionObserver observeSpy = vi.fn(); disconnectSpy = vi.fn(); (globalThis as any).IntersectionObserver = class IntersectionObserver { constructor(cb: any) { ioCallback = cb; } observe = observeSpy; unobserve = vi.fn(); disconnect = disconnectSpy; }; }); afterEach(() => { vi.clearAllMocks(); }); it('starts animation loop on mount', () => { const { unmount } = render(); expect(rafSpy).toHaveBeenCalled(); unmount(); expect(cancelRafSpy).toHaveBeenCalled(); }); it('pauses animation loop when off-screen and resumes when on-screen', () => { const { unmount } = render(); // Initial start expect(rafSpy).toHaveBeenCalledTimes(1); // Reset spies to check for subsequent calls rafSpy.mockClear(); cancelRafSpy.mockClear(); // Simulate off-screen act(() => { if (ioCallback) { ioCallback([{ isIntersecting: false } as IntersectionObserverEntry]); } }); // Should cancel animation frame expect(cancelRafSpy).toHaveBeenCalled(); // Should NOT request new frame (loop stopped) expect(rafSpy).not.toHaveBeenCalled(); // Reset spies cancelRafSpy.mockClear(); // Simulate on-screen act(() => { if (ioCallback) { ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]); } }); // Should restart animation loop expect(rafSpy).toHaveBeenCalled(); unmount(); expect(disconnectSpy).toHaveBeenCalled(); }); });