/**
* SmoothScroll lifecycle: the recursive requestAnimationFrame loop must be
* cancelled on unmount (it previously re-scheduled forever, calling lenis.raf()
* on a destroyed instance), and Lenis must not initialize under reduced-motion.
*/
import { render } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const destroy = vi.fn();
const lenisRaf = vi.fn();
const construct = vi.fn();
vi.mock('lenis', () => ({
default: class {
options = {};
constructor(opts: unknown) {
construct(opts);
}
raf(t: number) {
lenisRaf(t);
}
destroy() {
destroy();
}
},
}));
import { SmoothScroll } from '@/components/SmoothScroll';
function setReducedMotion(reduce: boolean) {
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
matches: reduce && query.includes('reduce'),
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
onchange: null,
dispatchEvent: vi.fn(),
}));
}
describe('SmoothScroll', () => {
beforeEach(() => {
construct.mockClear();
destroy.mockClear();
lenisRaf.mockClear();
});
afterEach(() => vi.restoreAllMocks());
it('cancels the rAF loop and destroys Lenis on unmount', () => {
setReducedMotion(false);
const cancelSpy = vi.spyOn(globalThis, 'cancelAnimationFrame');
const { unmount } = render(hi);
expect(construct).toHaveBeenCalledTimes(1);
unmount();
expect(destroy).toHaveBeenCalledTimes(1);
expect(cancelSpy).toHaveBeenCalled();
});
it('does not initialize Lenis under reduced-motion', () => {
setReducedMotion(true);
const { unmount } = render(hi);
expect(construct).not.toHaveBeenCalled();
unmount();
expect(destroy).not.toHaveBeenCalled();
});
});