⚡ Optimize GradientBlinds: Pause animation when off-screen #3
115
src/components/effects/GradientBlinds.test.tsx
Normal file
115
src/components/effects/GradientBlinds.test.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// @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(<GradientBlinds />);
|
||||||
|
expect(rafSpy).toHaveBeenCalled();
|
||||||
|
unmount();
|
||||||
|
expect(cancelRafSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pauses animation loop when off-screen and resumes when on-screen', () => {
|
||||||
|
const { unmount } = render(<GradientBlinds />);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -333,11 +333,28 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (!rafRef.current) {
|
||||||
|
lastTimeRef.current = 0;
|
||||||
|
rafRef.current = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rafRef.current) {
|
||||||
|
cancelAnimationFrame(rafRef.current);
|
||||||
|
rafRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(container);
|
||||||
|
|
||||||
rafRef.current = requestAnimationFrame(loop);
|
rafRef.current = requestAnimationFrame(loop);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||||
canvas.removeEventListener('pointermove', onPointerMove);
|
canvas.removeEventListener('pointermove', onPointerMove);
|
||||||
|
observer.disconnect();
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
if (canvas.parentElement === container) {
|
if (canvas.parentElement === container) {
|
||||||
container.removeChild(canvas);
|
container.removeChild(canvas);
|
||||||
|
|||||||
Reference in New Issue
Block a user