Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions __tests__/components/elements/BackToTop.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { render, fireEvent, act } from "@testing-library/react";
import BackToTop from "@/components/elements/BackToTop";

describe("BackToTop", () => {
beforeAll(() => {
// Mock window.scrollTo
Object.defineProperty(window, "scrollTo", {
value: jest.fn(),
writable: true,
});
// Mock requestAnimationFrame to execute callback immediately
jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb: FrameRequestCallback) => {
cb(0);
return 1;
});
jest.spyOn(window, "cancelAnimationFrame").mockImplementation(() => {});
});

afterAll(() => {
(window.requestAnimationFrame as jest.Mock).mockRestore();
(window.cancelAnimationFrame as jest.Mock).mockRestore();
});

it("should render nothing initially (scrollY <= 100)", () => {
const { container } = render(<BackToTop target="#top" />);
expect(container.firstChild).toBeNull();
});

it("should appear after scrolling down (> 100px)", () => {
const { container } = render(<BackToTop target="#top" />);

act(() => {
// Mock scrollY
Object.defineProperty(window, "scrollY", {
value: 200,
writable: true,
});
window.dispatchEvent(new Event("scroll"));
});

const button = container.querySelector(".paginacontainer");
expect(button).toBeInTheDocument();
});

it("should disappear after scrolling back up (<= 100px)", () => {
const { container } = render(<BackToTop target="#top" />);

// Scroll down first
act(() => {
Object.defineProperty(window, "scrollY", {
value: 200,
writable: true,
});
window.dispatchEvent(new Event("scroll"));
});

expect(container.querySelector(".paginacontainer")).toBeInTheDocument();

// Scroll back up
act(() => {
Object.defineProperty(window, "scrollY", {
value: 50,
writable: true,
});
window.dispatchEvent(new Event("scroll"));
});

expect(container.querySelector(".paginacontainer")).not.toBeInTheDocument();
});

it("should scroll to top when clicked", () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test suite is very thorough! To make it even more robust, consider adding a test case for the scenario where an invalid target selector is provided. This would ensure the fallback behavior (scrolling to the top) is explicitly tested.

// Mock the target element
document.body.innerHTML = '<div id="top"></div>';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Modifying document.body.innerHTML directly can lead to side effects between tests. For better test isolation, consider cleaning up the DOM after this test. A good way to do this is by adding an afterEach hook within your describe block to reset document.body.innerHTML.

const targetElement = document.getElementById("top");
if (targetElement) {
Object.defineProperty(targetElement, "offsetTop", {
value: 0,
writable: true,
});
}

const { container } = render(<BackToTop target="#top" />);

// Scroll down to make button visible
act(() => {
Object.defineProperty(window, "scrollY", {
value: 200,
writable: true,
});
window.dispatchEvent(new Event("scroll"));
});

const button = container.querySelector(".paginacontainer");
expect(button).toBeInTheDocument();

if (button) {
fireEvent.click(button);
expect(window.scrollTo).toHaveBeenCalledWith({
top: 0,
behavior: "smooth",
});
}
});
});
37 changes: 30 additions & 7 deletions components/elements/BackToTop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,42 @@ export default function BackToTop({ target }: any) {
const [hasScrolled, setHasScrolled] = useState(false);

useEffect(() => {
let ticking = false;
let rafId: number;

const onScroll = () => {
setHasScrolled(window.scrollY > 100);
if (!ticking) {
ticking = true;
rafId = window.requestAnimationFrame(() => {
setHasScrolled(window.scrollY > 100);
ticking = false;
});
}
};

window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
// Use passive listener for better scrolling performance
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll);
if (rafId) {
window.cancelAnimationFrame(rafId);
}
};
}, []);

const handleClick = () => {
window.scrollTo({
top: document.querySelector(target).offsetTop,
behavior: "smooth",
});
const targetElement = document.querySelector(target);
if (targetElement) {
window.scrollTo({
top: (targetElement as HTMLElement).offsetTop,
behavior: "smooth",
});
Comment on lines +33 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a type assertion with as HTMLElement works, but it's not the safest approach as it bypasses some type checking. A safer alternative is to use an instanceof check to ensure targetElement is indeed an HTMLElement before accessing its offsetTop property. This makes the code more robust.

Suggested change
if (targetElement) {
window.scrollTo({
top: (targetElement as HTMLElement).offsetTop,
behavior: "smooth",
});
if (targetElement instanceof HTMLElement) {
window.scrollTo({
top: targetElement.offsetTop,
behavior: "smooth",
});

} else {
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
};

return (
Expand Down
Loading
Loading