@shopify/shop-minis-react
Version:
React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)
266 lines (210 loc) • 7.2 kB
text/typescript
import {renderHook} from '@testing-library/react'
import {describe, expect, it, vi} from 'vitest'
import {useHandleAction} from './useHandleAction'
import type {ShopActionResult} from '@shopify/shop-minis-platform/actions'
describe('useHandleAction', () => {
describe('Success Case', () => {
it('returns data when action succeeds', async () => {
const mockData = {id: '123', name: 'Test'}
const mockAction = vi.fn(() =>
Promise.resolve({
ok: true as const,
data: mockData,
} as ShopActionResult<typeof mockData>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
const data = await result.current()
expect(data).toEqual(mockData)
expect(mockAction).toHaveBeenCalledTimes(1)
})
it('passes arguments to the action correctly', async () => {
const mockData = {success: true}
const mockAction = vi.fn((_arg1: string, _arg2: number) =>
Promise.resolve({
ok: true as const,
data: mockData,
} as ShopActionResult<typeof mockData>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
await result.current('test', 42)
expect(mockAction).toHaveBeenCalledWith('test', 42)
})
it('handles complex data structures', async () => {
const complexData = {
user: {
id: '1',
name: 'John',
addresses: [
{street: '123 Main St', city: 'New York'},
{street: '456 Oak Ave', city: 'Boston'},
],
},
metadata: {
timestamp: '2024-01-01',
version: 2,
},
}
const mockAction = vi.fn(() =>
Promise.resolve({
ok: true as const,
data: complexData,
} as ShopActionResult<typeof complexData>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
const data = await result.current()
expect(data).toEqual(complexData)
})
it('handles null/undefined data', async () => {
const mockAction = vi.fn(() =>
Promise.resolve({
ok: true as const,
data: null,
} as ShopActionResult<null>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
const data = await result.current()
expect(data).toBeNull()
})
})
describe('Error Case', () => {
it('throws error when action fails', async () => {
const mockError = {
code: 'ERROR_CODE',
message: 'Something went wrong',
}
const mockAction = vi.fn(() =>
Promise.resolve({
ok: false as const,
error: mockError,
} as ShopActionResult<any>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
await expect(result.current()).rejects.toEqual(mockError)
expect(mockAction).toHaveBeenCalledTimes(1)
})
it('preserves error structure', async () => {
const complexError = {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: {
fields: ['email', 'password'],
reasons: ['Invalid format', 'Too short'],
},
}
const mockAction = vi.fn(() =>
Promise.resolve({
ok: false as const,
error: complexError,
} as unknown as ShopActionResult<any>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
await expect(result.current()).rejects.toEqual(complexError)
})
it('handles string errors', async () => {
const mockAction = vi.fn(() =>
Promise.resolve({
ok: false as const,
error: 'Simple error message',
} as unknown as ShopActionResult<any>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
await expect(result.current()).rejects.toBe('Simple error message')
})
})
describe('Function Stability', () => {
it('maintains reference equality across renders', () => {
const mockAction = vi.fn(() =>
Promise.resolve({
ok: true as const,
data: 'test',
} as ShopActionResult<string>)
)
const {result, rerender} = renderHook(() => useHandleAction(mockAction))
const firstRender = result.current
rerender()
const secondRender = result.current
expect(firstRender).toBe(secondRender)
})
it('updates when action changes', () => {
const mockAction1 = vi.fn(() =>
Promise.resolve({
ok: true as const,
data: 'action1',
} as ShopActionResult<string>)
)
const mockAction2 = vi.fn(() =>
Promise.resolve({
ok: true as const,
data: 'action2',
} as ShopActionResult<string>)
)
const {result, rerender} = renderHook(
({action}) => useHandleAction(action),
{initialProps: {action: mockAction1}}
)
const firstRender = result.current
rerender({action: mockAction2})
const secondRender = result.current
expect(firstRender).not.toBe(secondRender)
})
})
describe('Multiple Calls', () => {
it('handles multiple concurrent calls', async () => {
let callCount = 0
const mockAction = vi.fn(async () => {
const currentCall = ++callCount
await new Promise(resolve => setTimeout(resolve, 10))
return {
ok: true as const,
data: currentCall,
} as ShopActionResult<number>
})
const {result} = renderHook(() => useHandleAction(mockAction))
const [result1, result2, result3] = await Promise.all([
result.current(),
result.current(),
result.current(),
])
expect(result1).toBe(1)
expect(result2).toBe(2)
expect(result3).toBe(3)
expect(mockAction).toHaveBeenCalledTimes(3)
})
it('handles sequential calls', async () => {
let counter = 0
const mockAction = vi.fn(
async () =>
({
ok: true as const,
data: ++counter,
}) as ShopActionResult<number>
)
const {result} = renderHook(() => useHandleAction(mockAction))
const result1 = await result.current()
const result2 = await result.current()
const result3 = await result.current()
expect(result1).toBe(1)
expect(result2).toBe(2)
expect(result3).toBe(3)
})
})
describe('Promise Behavior', () => {
it('returns a promise', () => {
const mockAction = vi.fn(() =>
Promise.resolve({
ok: true as const,
data: 'test',
} as ShopActionResult<string>)
)
const {result} = renderHook(() => useHandleAction(mockAction))
const returnValue = result.current()
expect(returnValue).toBeInstanceOf(Promise)
})
it('handles rejected promises from action', async () => {
const networkError = new Error('Network error')
const mockAction = vi.fn(() => Promise.reject(networkError))
const {result} = renderHook(() => useHandleAction(mockAction))
await expect(result.current()).rejects.toThrow('Network error')
})
})
})