@shopify/shop-minis-react
Version:
React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)
323 lines (274 loc) • 9.06 kB
text/typescript
import {renderHook, act} from '@testing-library/react'
import {describe, expect, it, vi, beforeEach} from 'vitest'
import {useShopActions} from '../../internal/useShopActions'
import {useImageUpload} from './useImageUpload'
// Mock dependencies
vi.mock('../../internal/useShopActions', () => ({
useShopActions: vi.fn(() => ({
createImageUploadLink: vi.fn(),
completeImageUpload: vi.fn(),
})),
}))
vi.mock('../../utils', () => ({
fileToDataUri: vi.fn((file: File) =>
Promise.resolve(`data:${file.type};base64,mockbase64data`)
),
}))
// Mock fetch globally
global.fetch = vi.fn()
describe('useImageUpload', () => {
let mockCreateImageUploadLink: ReturnType<typeof vi.fn>
let mockCompleteImageUpload: ReturnType<typeof vi.fn>
beforeEach(() => {
vi.clearAllMocks()
// Reset fetch mock with proper blob() implementation
;(global.fetch as any).mockImplementation(async (url: string) => {
// For data URI fetch (fileToDataUri result)
if (url.startsWith('data:')) {
return {
blob: async () => new Blob(['test image data'], {type: 'image/jpeg'}),
}
}
// Default for other fetches
return {
ok: true,
text: async () => 'Upload successful',
}
})
// Set up mock actions with proper implementations
mockCreateImageUploadLink = vi.fn().mockResolvedValue({
ok: true,
data: {
targets: [
{
url: 'https://storage.googleapis.com/upload',
resourceUrl: 'https://storage.googleapis.com/resource/123',
parameters: [
{name: 'key', value: 'test-key'},
{name: 'policy', value: 'test-policy'},
],
},
],
},
})
mockCompleteImageUpload = vi.fn().mockResolvedValue({
ok: true,
data: {
files: [
{
id: 'uploaded-image-id',
fileStatus: 'READY',
image: {
url: 'https://example.com/image.jpg',
},
},
],
},
})
// Update the mocks to return our mock actions
;(useShopActions as any).mockReturnValue({
createImageUploadLink: mockCreateImageUploadLink,
completeImageUpload: mockCompleteImageUpload,
})
})
describe('uploadImage', () => {
it('successfully uploads an image', async () => {
const {result} = renderHook(() => useImageUpload())
const testFile = new File(['test image'], 'test.jpg', {
type: 'image/jpeg',
})
let uploadedImages: any
await act(async () => {
uploadedImages = await result.current.uploadImage(testFile)
})
// Verify the upload flow
expect(mockCreateImageUploadLink).toHaveBeenCalledWith({
input: [
{
mimeType: 'image/jpeg',
fileSize: 10, // 'test image'.length
},
],
})
// Verify GCS upload
expect(global.fetch).toHaveBeenCalledWith(
'https://storage.googleapis.com/upload',
expect.objectContaining({
method: 'POST',
body: expect.any(FormData),
})
)
expect(mockCompleteImageUpload).toHaveBeenCalledWith({
resourceUrls: ['https://storage.googleapis.com/resource/123'],
})
expect(uploadedImages).toEqual([
{
id: 'uploaded-image-id',
imageUrl: 'https://example.com/image.jpg',
resourceUrl: 'https://storage.googleapis.com/resource/123',
},
])
})
it('throws error when createImageUploadLink fails', async () => {
mockCreateImageUploadLink.mockResolvedValue({
ok: false,
error: {
message: 'Failed to create upload link',
},
})
const {result} = renderHook(() => useImageUpload())
const testFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
await expect(
act(async () => {
await result.current.uploadImage(testFile)
})
).rejects.toThrow('Failed to create upload link')
})
it('throws error when GCS upload fails', async () => {
// Mock failed fetch for GCS upload
;(global.fetch as any).mockImplementation(async (url: string) => {
if (url.startsWith('data:')) {
return {
blob: async () => new Blob(['test'], {type: 'image/jpeg'}),
}
}
// GCS upload fails
return {
ok: false,
text: async () => 'Upload failed',
}
})
const {result} = renderHook(() => useImageUpload())
const testFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
await expect(
act(async () => {
await result.current.uploadImage(testFile)
})
).rejects.toThrow('Failed to upload image')
})
it('throws error when completeImageUpload fails', async () => {
mockCompleteImageUpload.mockResolvedValue({
ok: false,
error: {
message: 'Failed to complete upload',
},
})
const {result} = renderHook(() => useImageUpload())
const testFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
await expect(
act(async () => {
await result.current.uploadImage(testFile)
})
).rejects.toThrow('Failed to complete upload')
})
it('polls until image is ready', async () => {
// First two calls return PROCESSING, third returns READY
mockCompleteImageUpload
.mockResolvedValueOnce({
ok: true,
data: {
files: [
{
id: 'uploaded-image-id',
fileStatus: 'PROCESSING',
},
],
},
})
.mockResolvedValueOnce({
ok: true,
data: {
files: [
{
id: 'uploaded-image-id',
fileStatus: 'PROCESSING',
},
],
},
})
.mockResolvedValueOnce({
ok: true,
data: {
files: [
{
id: 'uploaded-image-id',
fileStatus: 'READY',
image: {
url: 'https://example.com/processed.jpg',
},
},
],
},
})
// Speed up test by mocking setTimeout
vi.useFakeTimers()
const {result} = renderHook(() => useImageUpload())
const testFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
// Start the upload without awaiting
let uploadPromise: Promise<any>
await act(async () => {
uploadPromise = result.current.uploadImage(testFile)
})
// Advance timers for polling
await act(async () => {
await vi.advanceTimersByTimeAsync(2000)
})
const uploadedImages = await uploadPromise!
expect(mockCompleteImageUpload).toHaveBeenCalledTimes(3)
expect(uploadedImages).toEqual([
{
id: 'uploaded-image-id',
imageUrl: 'https://example.com/processed.jpg',
resourceUrl: 'https://storage.googleapis.com/resource/123',
},
])
vi.useRealTimers()
})
it('handles file without initial size', async () => {
const {result} = renderHook(() => useImageUpload())
// Create a file without size property set
const testFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
let uploadedImages: any
await act(async () => {
uploadedImages = await result.current.uploadImage(testFile)
})
expect(uploadedImages).toEqual([
{
id: 'uploaded-image-id',
imageUrl: 'https://example.com/image.jpg',
resourceUrl: 'https://storage.googleapis.com/resource/123',
},
])
})
it('includes all form data parameters in GCS upload', async () => {
let capturedFormData: FormData | undefined
;(global.fetch as any).mockImplementation(
async (url: string, options: any) => {
if (url.startsWith('data:')) {
return {
blob: async () => new Blob(['test'], {type: 'image/jpeg'}),
}
}
// eslint-disable-next-line jest/no-if
if (url === 'https://storage.googleapis.com/upload') {
capturedFormData = options.body
}
return {
ok: true,
text: async () => 'Upload successful',
}
}
)
const {result} = renderHook(() => useImageUpload())
const testFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
await act(async () => {
await result.current.uploadImage(testFile)
})
// Verify FormData contains all expected fields
expect(capturedFormData).toBeDefined()
expect(capturedFormData?.get('key')).toBe('test-key')
expect(capturedFormData?.get('policy')).toBe('test-policy')
expect(capturedFormData?.get('file')).toBeDefined()
})
})
})