UNPKG

@shopify/shop-minis-react

Version:

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

227 lines (183 loc) 7.44 kB
import {renderHook, act} from '@testing-library/react' import {describe, expect, it, vi, beforeEach} from 'vitest' import {useHandleAction} from '../../internal/useHandleAction' import {useShopActions} from '../../internal/useShopActions' import {useAsyncStorage} from './useAsyncStorage' // Mock the internal hooks vi.mock('../../internal/useShopActions', () => ({ useShopActions: vi.fn(() => ({ getPersistedItem: vi.fn(), setPersistedItem: vi.fn(), removePersistedItem: vi.fn(), getAllPersistedKeys: vi.fn(), clearPersistedItems: vi.fn(), })), })) vi.mock('../../internal/useHandleAction', () => ({ useHandleAction: vi.fn((action: any) => action), })) describe('useAsyncStorage', () => { let mockActions: {[key: string]: ReturnType<typeof vi.fn>} beforeEach(() => { vi.clearAllMocks() // Set up mock actions with proper implementations mockActions = { getPersistedItem: vi.fn().mockResolvedValue(null), setPersistedItem: vi.fn().mockResolvedValue(undefined), removePersistedItem: vi.fn().mockResolvedValue(undefined), getAllPersistedKeys: vi.fn().mockResolvedValue(['key1', 'key2', 'key3']), clearPersistedItems: vi.fn().mockResolvedValue(undefined), } // Update the mock to return our mock actions ;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue(mockActions) // Make useHandleAction return the action directly ;(useHandleAction as ReturnType<typeof vi.fn>).mockImplementation( (action: any) => action ) }) describe('Hook Structure', () => { it('returns all expected methods', () => { const {result} = renderHook(() => useAsyncStorage()) expect(result.current).toHaveProperty('getItem') expect(result.current).toHaveProperty('setItem') expect(result.current).toHaveProperty('removeItem') expect(result.current).toHaveProperty('getAllKeys') expect(result.current).toHaveProperty('clear') expect(typeof result.current.getItem).toBe('function') expect(typeof result.current.setItem).toBe('function') expect(typeof result.current.removeItem).toBe('function') expect(typeof result.current.getAllKeys).toBe('function') expect(typeof result.current.clear).toBe('function') }) }) describe('getItem', () => { it('calls getPersistedItem with correct parameters', async () => { mockActions.getPersistedItem.mockResolvedValue('test-value') const {result} = renderHook(() => useAsyncStorage()) await act(async () => { const value = await result.current.getItem({key: 'test-key'}) expect(value).toBe('test-value') }) expect(mockActions.getPersistedItem).toHaveBeenCalledWith({ key: 'test-key', }) expect(mockActions.getPersistedItem).toHaveBeenCalledTimes(1) }) it('returns null when item does not exist', async () => { mockActions.getPersistedItem.mockResolvedValue(null) const {result} = renderHook(() => useAsyncStorage()) await act(async () => { const value = await result.current.getItem({key: 'non-existent'}) expect(value).toBeNull() }) }) }) describe('setItem', () => { it('calls setPersistedItem with correct parameters', async () => { const {result} = renderHook(() => useAsyncStorage()) await act(async () => { await result.current.setItem({key: 'test-key', value: 'test-value'}) }) expect(mockActions.setPersistedItem).toHaveBeenCalledWith({ key: 'test-key', value: 'test-value', }) expect(mockActions.setPersistedItem).toHaveBeenCalledTimes(1) }) it('handles empty string values', async () => { const {result} = renderHook(() => useAsyncStorage()) await act(async () => { await result.current.setItem({key: 'test-key', value: ''}) }) expect(mockActions.setPersistedItem).toHaveBeenCalledWith({ key: 'test-key', value: '', }) }) }) describe('removeItem', () => { it('calls removePersistedItem with correct parameters', async () => { const {result} = renderHook(() => useAsyncStorage()) await act(async () => { await result.current.removeItem({key: 'test-key'}) }) expect(mockActions.removePersistedItem).toHaveBeenCalledWith({ key: 'test-key', }) expect(mockActions.removePersistedItem).toHaveBeenCalledTimes(1) }) }) describe('getAllKeys', () => { it('returns all storage keys', async () => { const {result} = renderHook(() => useAsyncStorage()) await act(async () => { const keys = await result.current.getAllKeys() expect(keys).toEqual(['key1', 'key2', 'key3']) }) expect(mockActions.getAllPersistedKeys).toHaveBeenCalledWith() expect(mockActions.getAllPersistedKeys).toHaveBeenCalledTimes(1) }) it('returns empty array when no keys exist', async () => { mockActions.getAllPersistedKeys.mockResolvedValue([]) const {result} = renderHook(() => useAsyncStorage()) await act(async () => { const keys = await result.current.getAllKeys() expect(keys).toEqual([]) }) }) }) describe('clear', () => { it('calls clearPersistedItems', async () => { const {result} = renderHook(() => useAsyncStorage()) await act(async () => { await result.current.clear() }) expect(mockActions.clearPersistedItems).toHaveBeenCalledWith() expect(mockActions.clearPersistedItems).toHaveBeenCalledTimes(1) }) }) describe('Error Handling', () => { it('propagates errors from getItem', async () => { const error = new Error('Storage error') mockActions.getPersistedItem.mockRejectedValue(error) const {result} = renderHook(() => useAsyncStorage()) await act(async () => { await expect(result.current.getItem({key: 'test-key'})).rejects.toThrow( 'Storage error' ) }) }) it('propagates errors from setItem', async () => { const error = new Error('Write error') mockActions.setPersistedItem.mockRejectedValue(error) const {result} = renderHook(() => useAsyncStorage()) await act(async () => { await expect( result.current.setItem({key: 'test-key', value: 'test-value'}) ).rejects.toThrow('Write error') }) }) it('propagates errors from clear', async () => { const error = new Error('Clear error') mockActions.clearPersistedItems.mockRejectedValue(error) const {result} = renderHook(() => useAsyncStorage()) await act(async () => { await expect(result.current.clear()).rejects.toThrow('Clear error') }) }) }) describe('Stability', () => { it('maintains function reference stability across renders', () => { const {result, rerender} = renderHook(() => useAsyncStorage()) const firstRender = {...result.current} rerender() const secondRender = {...result.current} // Functions should maintain reference equality expect(firstRender.getItem).toBe(secondRender.getItem) expect(firstRender.setItem).toBe(secondRender.setItem) expect(firstRender.removeItem).toBe(secondRender.removeItem) expect(firstRender.getAllKeys).toBe(secondRender.getAllKeys) expect(firstRender.clear).toBe(secondRender.clear) }) }) })