From 96309b2b7da398726014271a260854ddce6e0a33 Mon Sep 17 00:00:00 2001 From: tungulin Date: Thu, 8 Jan 2026 19:07:19 +0300 Subject: [PATCH] test: useSticky --- .../src/hooks/useSticky/useSticky.test.ts | 169 ++++++++++++++++++ .../core/src/hooks/useSticky/useSticky.ts | 16 +- packages/core/tests/setupTests.ts | 8 +- 3 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/hooks/useSticky/useSticky.test.ts diff --git a/packages/core/src/hooks/useSticky/useSticky.test.ts b/packages/core/src/hooks/useSticky/useSticky.test.ts new file mode 100644 index 00000000..7d83196b --- /dev/null +++ b/packages/core/src/hooks/useSticky/useSticky.test.ts @@ -0,0 +1,169 @@ +import { act, renderHook } from '@testing-library/react'; +import { useSticky, UseStickyReturn } from './useSticky'; +import { renderHookServer } from '@/tests'; +import { target } from '@/utils/helpers'; +import { StateRef } from '../state'; + +const DEFAULT_MOCK_BOUNDING_DATA = { + top: 0, + left: 0, + bottom: 200, + right: 0, + width: 100, + height: 100, + x: 0, + y: 100, + toJSON: () => {} +}; + +const targets = [ + undefined, + target('#target'), + target(document.getElementById('target')!), + target(() => document.getElementById('target')!), + { current: document.getElementById('target') } +]; + +const root = target(document.getElementById('target-root')!); +const element = document.getElementById('target') as HTMLDivElement; + +const getBoundingClientRectSpy = vi.spyOn(element, 'getBoundingClientRect'); + +targets.forEach((target) => { + describe(`${target}`, () => { + it('Should use sticky', () => { + const { result } = renderHook(() => { + if (target) + return useSticky(target) as unknown as { + ref: StateRef; + stuck: UseStickyReturn; + }; + return useSticky(); + }); + + if (!target) { + expect(result.current.ref).toBeTypeOf('function'); + expect(result.current.stuck).toBeTypeOf('boolean'); + } else { + expect(result.current).toBeTypeOf('boolean'); + } + }); + + it('Should use sticky on server side', () => { + const { result } = renderHookServer(() => { + if (target) + return useSticky(target) as unknown as { + ref: StateRef; + stuck: UseStickyReturn; + }; + return useSticky(); + }); + + if (!target) { + expect(result.current.ref).toBeTypeOf('function'); + expect(result.current.stuck).toBeTypeOf('boolean'); + } else { + expect(result.current).toBeTypeOf('boolean'); + } + }); + + it('Should update stuck element', () => { + const { result } = renderHook(() => { + if (target) + return useSticky(target, { root }) as unknown as { + ref: StateRef; + stuck: UseStickyReturn; + }; + return useSticky(); + }); + + act(() => { + getBoundingClientRectSpy.mockReturnValue({ + ...DEFAULT_MOCK_BOUNDING_DATA, + top: 200 + }); + window.dispatchEvent(new Event('resize')); + }); + + if (target) { + expect(result.current).toBe(false); + } + + act(() => { + getBoundingClientRectSpy.mockReturnValue({ + ...DEFAULT_MOCK_BOUNDING_DATA, + top: 0 + }); + window.dispatchEvent(new Event('resize')); + }); + + if (target) { + expect(result.current).toBe(true); + } + }); + + it('Should update stuck element with horizontal axis', () => { + const { result } = renderHook(() => { + if (target) + return useSticky(target, { + root, + axis: 'horizontal' + }) as unknown as { + ref: StateRef; + stuck: UseStickyReturn; + }; + return useSticky(); + }); + + act(() => { + getBoundingClientRectSpy.mockReturnValue({ + ...DEFAULT_MOCK_BOUNDING_DATA, + left: 200 + }); + window.dispatchEvent(new Event('resize')); + }); + + if (target) { + expect(result.current).toBe(false); + } + + act(() => { + getBoundingClientRectSpy.mockReturnValue({ + ...DEFAULT_MOCK_BOUNDING_DATA, + left: 0 + }); + window.dispatchEvent(new Event('resize')); + }); + + if (target) { + expect(result.current).toBe(true); + } + }); + + it('Should clean up on unmount', () => { + const removeEventListenerWindowSpy = vi.spyOn(window, 'removeEventListener'); + + const removeEventListenerSpy = vi.spyOn(document.documentElement, 'removeEventListener'); + + const { result, unmount } = renderHook(() => { + if (target) + return useSticky(target) as unknown as { + ref: StateRef; + stuck: UseStickyReturn; + }; + return useSticky(); + }); + + if (!target) act(() => result.current.ref(element)); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function)); + expect(removeEventListenerWindowSpy).toHaveBeenCalledWith('resize', expect.any(Function)); + expect(removeEventListenerWindowSpy).toHaveBeenCalledWith( + 'orientationchange', + expect.any(Function) + ); + }); + }); +}); diff --git a/packages/core/src/hooks/useSticky/useSticky.ts b/packages/core/src/hooks/useSticky/useSticky.ts index 722e30c3..8625e2ff 100644 --- a/packages/core/src/hooks/useSticky/useSticky.ts +++ b/packages/core/src/hooks/useSticky/useSticky.ts @@ -71,20 +71,26 @@ export const useSticky = ((...params: any[]) => { if (!element) return; - const root = (options?.root ? isTarget.getElement(options.root) : document) as Element; - const elementOffsetTop = - element.getBoundingClientRect().top + root.scrollTop - root.getBoundingClientRect().top; - const elementOffsetLeft = - element.getBoundingClientRect().left + root.scrollLeft - root.getBoundingClientRect().left; + const root = ( + options?.root ? isTarget.getElement(options.root) : document.documentElement + ) as Element; const onSticky = () => { if (axis === 'vertical') { const scrollTop = root.scrollTop; + + const elementOffsetTop = + element.getBoundingClientRect().top + scrollTop - root.getBoundingClientRect().top; + setStuck(scrollTop >= elementOffsetTop); } if (axis === 'horizontal') { const scrollLeft = root.scrollLeft; + + const elementOffsetLeft = + element.getBoundingClientRect().left + scrollLeft - root.getBoundingClientRect().left; + setStuck(scrollLeft >= elementOffsetLeft); } }; diff --git a/packages/core/tests/setupTests.ts b/packages/core/tests/setupTests.ts index 06a59d86..1b566d61 100644 --- a/packages/core/tests/setupTests.ts +++ b/packages/core/tests/setupTests.ts @@ -1,9 +1,15 @@ if (typeof document !== 'undefined') { + const targetRoot = document.createElement('div'); const target = document.createElement('div'); + + targetRoot.id = 'target-root'; + target.id = 'target'; target.tabIndex = 0; target.textContent = 'target'; - document.body.appendChild(target); + + targetRoot.appendChild(target); + document.body.appendChild(targetRoot); } afterEach(() => {