UNPKG

@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
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) }) }) })