UNPKG

@shopify/shop-minis-react

Version:

React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)

425 lines (338 loc) 12.1 kB
import {renderHook, act} from '@testing-library/react' import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest' import {useNavigateWithTransition} from './useNavigateWithTransition' // Mock react-router const mockNavigate = vi.fn() const mockLocation = {pathname: '/current'} vi.mock('react-router', () => ({ useNavigate: () => mockNavigate, useLocation: () => mockLocation, NavigateOptions: {}, })) describe('useNavigateWithTransition', () => { let originalStartViewTransition: any beforeEach(() => { vi.clearAllMocks() originalStartViewTransition = document.startViewTransition mockLocation.pathname = '/current' }) afterEach(() => { document.startViewTransition = originalStartViewTransition document.documentElement.removeAttribute('data-navigation-type') }) describe('Without View Transitions API', () => { beforeEach(() => { // Remove startViewTransition to simulate unsupported browser ;(document as any).startViewTransition = undefined }) it('navigates directly when view transitions not supported', () => { const {result} = renderHook(() => useNavigateWithTransition()) act(() => { result.current('/new-route') }) expect(mockNavigate).toHaveBeenCalledWith('/new-route', undefined) }) it('navigates with options when view transitions not supported', () => { const {result} = renderHook(() => useNavigateWithTransition()) const options = {replace: true, state: {from: 'test'}} act(() => { result.current('/new-route', options) }) expect(mockNavigate).toHaveBeenCalledWith('/new-route', options) }) it('handles delta navigation without view transitions', () => { const {result} = renderHook(() => useNavigateWithTransition()) act(() => { result.current(-1) }) expect(mockNavigate).toHaveBeenCalledWith(-1) }) }) describe('With View Transitions API', () => { let mockTransition: any beforeEach(() => { mockTransition = { finished: Promise.resolve(), ready: Promise.resolve(), types: new Set<string>(), updateCallbackDone: Promise.resolve(), skipTransition: () => {}, } // Mock startViewTransition document.startViewTransition = vi.fn((callback: () => void) => { callback() return mockTransition }) as any }) it('uses view transition for path navigation', async () => { const scrollToSpy = vi.spyOn(window, 'scrollTo') const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/new-route') await mockTransition.finished }) expect(document.startViewTransition).toHaveBeenCalled() expect(mockNavigate).toHaveBeenCalledWith('/new-route', { replace: false, }) expect(scrollToSpy).toHaveBeenCalledWith(0, 0) scrollToSpy.mockRestore() }) it('uses replace navigation for same route', async () => { const scrollToSpy = vi.spyOn(window, 'scrollTo') const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/current') await mockTransition.finished }) expect(document.startViewTransition).toHaveBeenCalled() expect(mockNavigate).toHaveBeenCalledWith('/current', { replace: true, }) expect(scrollToSpy).toHaveBeenCalledWith(0, 0) scrollToSpy.mockRestore() }) it('merges custom options with defaults', async () => { const scrollToSpy = vi.spyOn(window, 'scrollTo') const {result} = renderHook(() => useNavigateWithTransition()) const customOptions = {state: {from: 'test'}, replace: true} await act(async () => { result.current('/new-route', customOptions) await mockTransition.finished }) expect(mockNavigate).toHaveBeenCalledWith('/new-route', { replace: true, state: {from: 'test'}, }) expect(scrollToSpy).toHaveBeenCalledWith(0, 0) scrollToSpy.mockRestore() }) it('removes navigation type attribute after transition completes', async () => { const removeAttributeSpy = vi.spyOn( document.documentElement, 'removeAttribute' ) const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/new-route') await mockTransition.finished }) expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type') }) it('handles view transition errors gracefully', async () => { const consoleErrorSpy = vi .spyOn(console, 'error') .mockImplementation(() => {}) const errorTransition = { finished: Promise.reject(new Error('Transition failed')), ready: Promise.resolve(), types: new Set<string>(), updateCallbackDone: Promise.resolve(), skipTransition: () => {}, } document.startViewTransition = vi.fn((callback: () => void) => { callback() return errorTransition }) as any const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/new-route') try { await errorTransition.finished } catch { // Expected error } }) expect(consoleErrorSpy).toHaveBeenCalledWith( 'View transition error:', expect.any(Error) ) consoleErrorSpy.mockRestore() }) it('uses view transition for delta navigation', async () => { const scrollToSpy = vi.spyOn(window, 'scrollTo') const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current(-1) await mockTransition.finished }) expect(document.startViewTransition).toHaveBeenCalled() expect(mockNavigate).toHaveBeenCalledWith(-1) // Delta navigation should NOT reset scroll expect(scrollToSpy).not.toHaveBeenCalled() scrollToSpy.mockRestore() }) it('removes attribute after delta navigation transition', async () => { const removeAttributeSpy = vi.spyOn( document.documentElement, 'removeAttribute' ) const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current(-2) await mockTransition.finished }) expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type') }) it('handles positive delta navigation', async () => { const scrollToSpy = vi.spyOn(window, 'scrollTo') const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current(1) await mockTransition.finished }) expect(document.startViewTransition).toHaveBeenCalled() expect(mockNavigate).toHaveBeenCalledWith(1) // Delta navigation should NOT reset scroll expect(scrollToSpy).not.toHaveBeenCalled() scrollToSpy.mockRestore() }) it('does not reset scroll when preventScrollReset is true', async () => { const scrollToSpy = vi.spyOn(window, 'scrollTo') const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/new-route', {preventScrollReset: true}) await mockTransition.finished }) expect(document.startViewTransition).toHaveBeenCalled() expect(mockNavigate).toHaveBeenCalledWith('/new-route', { replace: false, preventScrollReset: true, }) expect(scrollToSpy).not.toHaveBeenCalled() scrollToSpy.mockRestore() }) }) describe('Edge Cases', () => { beforeEach(() => { document.startViewTransition = vi.fn((callback: () => void) => { callback() return { finished: Promise.resolve(), ready: Promise.resolve(), types: new Set<string>(), updateCallbackDone: Promise.resolve(), skipTransition: () => {}, } }) as any }) it('handles navigation to empty string', async () => { const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('') }) expect(mockNavigate).toHaveBeenCalledWith('', { replace: false, }) }) it('handles navigation to root path using history delta', async () => { // Mock history state with idx const originalHistoryState = window.history.state Object.defineProperty(window.history, 'state', { value: {idx: 3}, writable: true, configurable: true, }) const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/') }) // Should navigate back using delta based on history index expect(mockNavigate).toHaveBeenCalledWith(-3) // Restore original state Object.defineProperty(window.history, 'state', { value: originalHistoryState, writable: true, configurable: true, }) }) it('handles navigation with query parameters', async () => { const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/search?q=test&page=2') }) expect(mockNavigate).toHaveBeenCalledWith('/search?q=test&page=2', { replace: false, }) }) it('handles navigation with hash', async () => { const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/page#section') }) expect(mockNavigate).toHaveBeenCalledWith('/page#section', { replace: false, }) }) it('handles zero delta navigation', async () => { const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current(0) }) expect(document.startViewTransition).toHaveBeenCalled() expect(mockNavigate).toHaveBeenCalledWith(0) }) }) describe('Multiple Navigations', () => { beforeEach(() => { const transitions: any[] = [] document.startViewTransition = vi.fn((callback: () => void) => { callback() const transition = { finished: new Promise(resolve => setTimeout(resolve, 10)), ready: Promise.resolve(), types: new Set<string>(), updateCallbackDone: Promise.resolve(), skipTransition: () => {}, } transitions.push(transition) return transition }) as any }) it('handles multiple sequential navigations', async () => { const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/route1') result.current('/route2') result.current('/route3') }) expect(document.startViewTransition).toHaveBeenCalledTimes(3) expect(mockNavigate).toHaveBeenCalledTimes(3) expect(mockNavigate).toHaveBeenNthCalledWith( 1, '/route1', expect.any(Object) ) expect(mockNavigate).toHaveBeenNthCalledWith( 2, '/route2', expect.any(Object) ) expect(mockNavigate).toHaveBeenNthCalledWith( 3, '/route3', expect.any(Object) ) }) it('handles mixed path and delta navigations', async () => { const {result} = renderHook(() => useNavigateWithTransition()) await act(async () => { result.current('/forward') result.current(-1) result.current('/another') }) expect(document.startViewTransition).toHaveBeenCalledTimes(3) expect(mockNavigate).toHaveBeenNthCalledWith( 1, '/forward', expect.any(Object) ) expect(mockNavigate).toHaveBeenNthCalledWith(2, -1) expect(mockNavigate).toHaveBeenNthCalledWith( 3, '/another', expect.any(Object) ) }) }) })