@shopify/shop-minis-react
Version:
React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)
353 lines (289 loc) • 10.5 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 {useImageUpload} from '../storage/useImageUpload'
import {useCreateImageContent} from './useCreateImageContent'
// Mock the internal hooks and utilities
vi.mock('../../internal/useShopActions', () => ({
useShopActions: vi.fn(() => ({
createContent: vi.fn(),
})),
}))
vi.mock('../../internal/useHandleAction', () => ({
useHandleAction: vi.fn((action: any) => action),
}))
vi.mock('../storage/useImageUpload', () => ({
useImageUpload: vi.fn(() => ({
uploadImage: vi.fn(),
})),
}))
describe('useCreateImageContent', () => {
let mockCreateContent: ReturnType<typeof vi.fn>
let mockUploadImage: ReturnType<typeof vi.fn>
beforeEach(() => {
vi.clearAllMocks()
// Set up mock actions with proper implementations
mockCreateContent = vi.fn().mockResolvedValue({
data: {
publicId: 'content-123',
image: {
id: 'img-123',
url: 'https://example.com/content-image.jpg',
width: 800,
height: 600,
},
title: 'Test Content',
visibility: ['DISCOVERABLE'],
},
})
mockUploadImage = vi.fn().mockResolvedValue([
{
id: 'upload-123',
imageUrl: 'https://example.com/uploaded-image.jpg',
resourceUrl: 'https://example.com/resource/123',
},
])
// Update the mocks to return our mock actions
;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue({
createContent: mockCreateContent,
})
;(useImageUpload as ReturnType<typeof vi.fn>).mockReturnValue({
uploadImage: mockUploadImage,
})
// Make useHandleAction return the action directly
;(useHandleAction as ReturnType<typeof vi.fn>).mockImplementation(
(action: any) => action
)
})
describe('Hook Structure', () => {
it('returns expected properties', () => {
const {result} = renderHook(() => useCreateImageContent())
expect(result.current).toHaveProperty('createImageContent')
expect(result.current).toHaveProperty('loading')
expect(typeof result.current.createImageContent).toBe('function')
expect(typeof result.current.loading).toBe('boolean')
})
it('initializes with loading false', () => {
const {result} = renderHook(() => useCreateImageContent())
expect(result.current.loading).toBe(false)
})
})
describe('createImageContent', () => {
it('successfully creates image content', async () => {
const {result} = renderHook(() => useCreateImageContent())
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
const params = {
image: imageFile,
contentTitle: 'Test Content',
visibility: ['DISCOVERABLE'] as any,
}
await act(async () => {
const content = await result.current.createImageContent(params)
expect(content.data).toEqual({
publicId: 'content-123',
image: {
id: 'img-123',
url: 'https://example.com/content-image.jpg',
width: 800,
height: 600,
},
title: 'Test Content',
visibility: ['DISCOVERABLE'],
})
})
// Verify upload was called
expect(mockUploadImage).toHaveBeenCalledWith(imageFile)
expect(mockUploadImage).toHaveBeenCalledTimes(1)
// Verify createContent was called with correct params
expect(mockCreateContent).toHaveBeenCalledWith({
title: 'Test Content',
imageUrl: 'https://example.com/uploaded-image.jpg',
visibility: ['DISCOVERABLE'],
})
expect(mockCreateContent).toHaveBeenCalledTimes(1)
})
it('sets loading state during operation', async () => {
const {result} = renderHook(() => useCreateImageContent())
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
const params = {
image: imageFile,
contentTitle: 'Test Content',
}
// Add a delay to the mock to test loading state
let resolveUpload: any
mockUploadImage.mockImplementation(
() =>
new Promise(resolve => {
resolveUpload = resolve
})
)
// Start the operation
let createPromise: Promise<any>
act(() => {
createPromise = result.current.createImageContent(params)
})
// Check loading state is true while operation is in progress
expect(result.current.loading).toBe(true)
// Complete the upload
await act(async () => {
resolveUpload([
{
id: 'upload-123',
imageUrl: 'https://example.com/uploaded-image.jpg',
},
])
await createPromise
})
// Loading should be false after completion
expect(result.current.loading).toBe(false)
})
it('throws error for missing file type', async () => {
const {result} = renderHook(() => useCreateImageContent())
// Create a file without a type
const imageFile = new File(['test'], 'test.jpg')
Object.defineProperty(imageFile, 'type', {value: undefined})
const params = {
image: imageFile,
contentTitle: 'Test Content',
}
await act(async () => {
await expect(result.current.createImageContent(params)).rejects.toThrow(
'Unable to determine file type'
)
})
expect(mockUploadImage).not.toHaveBeenCalled()
expect(mockCreateContent).not.toHaveBeenCalled()
})
it('throws error for non-image file type', async () => {
const {result} = renderHook(() => useCreateImageContent())
const textFile = new File(['test'], 'test.txt', {type: 'text/plain'})
const params = {
image: textFile,
contentTitle: 'Test Content',
}
await act(async () => {
await expect(result.current.createImageContent(params)).rejects.toThrow(
'Invalid file type: must be an image'
)
})
expect(mockUploadImage).not.toHaveBeenCalled()
expect(mockCreateContent).not.toHaveBeenCalled()
})
it('throws error when image upload fails', async () => {
const {result} = renderHook(() => useCreateImageContent())
// Mock upload to return no URL
mockUploadImage.mockResolvedValue([
{
id: 'upload-123',
imageUrl: undefined,
resourceUrl: 'https://example.com/resource/123',
},
])
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
const params = {
image: imageFile,
contentTitle: 'Test Content',
}
await act(async () => {
await expect(result.current.createImageContent(params)).rejects.toThrow(
'Image upload failed'
)
})
expect(mockUploadImage).toHaveBeenCalledWith(imageFile)
expect(mockCreateContent).not.toHaveBeenCalled()
})
it('handles content creation with null visibility', async () => {
const {result} = renderHook(() => useCreateImageContent())
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
const params = {
image: imageFile,
contentTitle: 'Test Content',
visibility: null,
}
await act(async () => {
await result.current.createImageContent(params)
})
expect(mockCreateContent).toHaveBeenCalledWith({
title: 'Test Content',
imageUrl: 'https://example.com/uploaded-image.jpg',
visibility: null,
})
})
it('returns user errors from content creation', async () => {
const {result} = renderHook(() => useCreateImageContent())
mockCreateContent.mockResolvedValue({
data: {
publicId: 'content-123',
title: 'Test Content',
},
userErrors: [
{
field: 'visibility',
message: 'Invalid visibility',
},
],
})
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
const params = {
image: imageFile,
contentTitle: 'Test Content',
}
await act(async () => {
const contentResult = await result.current.createImageContent(params)
expect(contentResult.userErrors).toEqual([
{
field: 'visibility',
message: 'Invalid visibility',
},
])
})
})
})
describe('Error Handling', () => {
it('handles upload error properly', async () => {
const {result} = renderHook(() => useCreateImageContent())
mockUploadImage.mockRejectedValue(new Error('Upload failed'))
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
const params = {
image: imageFile,
contentTitle: 'Test Content',
}
// Check that error is thrown
await act(async () => {
await expect(result.current.createImageContent(params)).rejects.toThrow(
'Upload failed'
)
})
// Loading state is only managed during successful operations
// The hook doesn't reset loading on error since it's controlled by the consumer
})
it('handles content creation error properly', async () => {
const {result} = renderHook(() => useCreateImageContent())
mockCreateContent.mockRejectedValue(new Error('Creation failed'))
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
const params = {
image: imageFile,
contentTitle: 'Test Content',
}
// Check that error is thrown
await act(async () => {
await expect(result.current.createImageContent(params)).rejects.toThrow(
'Creation failed'
)
})
// Loading state is only managed during successful operations
// The hook doesn't reset loading on error since it's controlled by the consumer
})
})
describe('Stability', () => {
it('maintains function reference stability across renders', () => {
const {result, rerender} = renderHook(() => useCreateImageContent())
const firstRender = result.current.createImageContent
rerender()
const secondRender = result.current.createImageContent
// Function should maintain reference equality
expect(firstRender).toBe(secondRender)
})
})
})