⚡ Perf: Throttle Hero scroll listener #4
135
src/components/sections/Hero.test.tsx
Normal file
135
src/components/sections/Hero.test.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { render, fireEvent, act, cleanup } from "@testing-library/react";
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { Hero } from "./Hero";
|
||||||
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock("../../i18n", () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: {
|
||||||
|
hero: {
|
||||||
|
company: "Test Company",
|
||||||
|
greeting: "Hello",
|
||||||
|
tagline: "Building the Future",
|
||||||
|
rotatingWords: ["Code", "Design"],
|
||||||
|
cta: "Contact Us",
|
||||||
|
ctaSecondary: "About Us",
|
||||||
|
scroll: "Scroll Down",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../hooks", () => ({
|
||||||
|
useTypingEffect: () => ({
|
||||||
|
text: "Code",
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../effects", () => ({
|
||||||
|
GradientBlinds: () => <div data-testid="gradient-blinds" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock Link from react-router-dom to avoid needing Router context for just Link,
|
||||||
|
// OR just use MemoryRouter in the render. MemoryRouter is safer.
|
||||||
|
|
||||||
|
describe("Hero Component", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset scroll position
|
||||||
|
window.scrollY = 0;
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders correctly and shows scroll indicator initially", () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<Hero />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText("Test Company")).toBeDefined();
|
||||||
|
expect(getByText("Scroll Down")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides scroll indicator when scrolled down", () => {
|
||||||
|
const { queryByText } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<Hero />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText("Scroll Down")).toBeDefined();
|
||||||
|
|
||||||
|
// Simulate scroll
|
||||||
|
act(() => {
|
||||||
|
window.scrollY = 100;
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
|
vi.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByText("Scroll Down")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows scroll indicator again when scrolled back up", () => {
|
||||||
|
const { queryByText } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<Hero />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial scroll down
|
||||||
|
act(() => {
|
||||||
|
window.scrollY = 100;
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
|
vi.runAllTimers();
|
||||||
|
});
|
||||||
|
expect(queryByText("Scroll Down")).toBeNull();
|
||||||
|
|
||||||
|
// Scroll back up
|
||||||
|
act(() => {
|
||||||
|
window.scrollY = 0;
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
|
vi.runAllTimers();
|
||||||
|
});
|
||||||
|
expect(queryByText("Scroll Down")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throttles scroll events using requestAnimationFrame", () => {
|
||||||
|
const rafSpy = vi.spyOn(window, "requestAnimationFrame");
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<Hero />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger multiple scroll events rapidly
|
||||||
|
act(() => {
|
||||||
|
window.scrollY = 10;
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should call rAF only once before timers run (because of ticking flag)
|
||||||
|
expect(rafSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Run timers to reset ticking
|
||||||
|
act(() => {
|
||||||
|
vi.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger again
|
||||||
|
act(() => {
|
||||||
|
window.dispatchEvent(new Event("scroll"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should call rAF again
|
||||||
|
expect(rafSpy).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,12 +14,24 @@ export function Hero() {
|
|||||||
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
|
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let ticking = false;
|
||||||
|
let rafId: number;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
if (!ticking) {
|
||||||
|
rafId = window.requestAnimationFrame(() => {
|
||||||
setShowScrollIndicator(window.scrollY < 50);
|
setShowScrollIndicator(window.scrollY < 50);
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
if (rafId) window.cancelAnimationFrame(rafId);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { text } = useTypingEffect({
|
const { text } = useTypingEffect({
|
||||||
|
|||||||
Reference in New Issue
Block a user