@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
text/typescript
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)
})
})
})