@shopify/shop-minis-react 0.0.34 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index5.js +2 -3
- package/dist/_virtual/index5.js.map +1 -1
- package/dist/_virtual/index6.js +2 -2
- package/dist/_virtual/index7.js +3 -2
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/components/atoms/alert-dialog.js.map +1 -1
- package/dist/components/atoms/icon-button.js +12 -12
- package/dist/components/atoms/icon-button.js.map +1 -1
- package/dist/components/atoms/text-input.js +22 -0
- package/dist/components/atoms/text-input.js.map +1 -0
- package/dist/components/commerce/merchant-card.js +1 -0
- package/dist/components/commerce/merchant-card.js.map +1 -1
- package/dist/components/ui/input.js +15 -9
- package/dist/components/ui/input.js.map +1 -1
- package/dist/hooks/util/useKeyboardAvoidingView.js +23 -0
- package/dist/hooks/util/useKeyboardAvoidingView.js.map +1 -0
- package/dist/index.js +226 -222
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +4 -1
- package/dist/mocks.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/dist/utils/image.js +5 -4
- package/dist/utils/image.js.map +1 -1
- package/package.json +21 -4
- package/src/components/atoms/alert-dialog.test.tsx +67 -0
- package/src/components/atoms/alert-dialog.tsx +13 -11
- package/src/components/atoms/favorite-button.test.tsx +56 -0
- package/src/components/atoms/icon-button.tsx +1 -1
- package/src/components/atoms/image.test.tsx +108 -0
- package/src/components/atoms/product-variant-price.test.tsx +128 -0
- package/src/components/atoms/text-input.test.tsx +104 -0
- package/src/components/atoms/text-input.tsx +31 -0
- package/src/components/commerce/merchant-card.test.tsx +261 -0
- package/src/components/commerce/merchant-card.tsx +2 -0
- package/src/components/commerce/product-card.test.tsx +364 -0
- package/src/components/commerce/product-link.test.tsx +483 -0
- package/src/components/commerce/quantity-selector.test.tsx +382 -0
- package/src/components/commerce/search.test.tsx +487 -0
- package/src/components/content/image-content-wrapper.test.tsx +92 -0
- package/src/components/index.ts +1 -0
- package/src/components/navigation/transition-link.test.tsx +155 -0
- package/src/components/ui/input.test.tsx +21 -0
- package/src/components/ui/input.tsx +10 -1
- package/src/hooks/content/useCreateImageContent.test.ts +352 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/navigation/useNavigateWithTransition.test.ts +371 -0
- package/src/hooks/navigation/useViewTransitions.test.ts +469 -0
- package/src/hooks/product/useProductSearch.test.ts +470 -0
- package/src/hooks/storage/useAsyncStorage.test.ts +225 -0
- package/src/hooks/storage/useImageUpload.test.ts +322 -0
- package/src/hooks/util/useKeyboardAvoidingView.ts +37 -0
- package/src/internal/useHandleAction.test.ts +265 -0
- package/src/internal/useShopActionsDataFetching.test.ts +465 -0
- package/src/mocks.ts +3 -1
- package/src/providers/ImagePickerProvider.test.tsx +467 -0
- package/src/stories/ProductCard.stories.tsx +2 -2
- package/src/stories/TextInput.stories.tsx +26 -0
- package/src/test-setup.ts +34 -0
- package/src/test-utils.tsx +167 -0
- package/src/utils/image.ts +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {render, screen, userEvent} from '../../test-utils'
|
|
4
|
+
|
|
5
|
+
import {TransitionLink} from './transition-link'
|
|
6
|
+
|
|
7
|
+
// Mock react-router hooks
|
|
8
|
+
const mockNavigate = vi.fn()
|
|
9
|
+
vi.mock('react-router', () => ({
|
|
10
|
+
useHref: vi.fn((to: string) => to),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
// Mock navigation hook
|
|
14
|
+
vi.mock('../../hooks/navigation/useNavigateWithTransition', () => ({
|
|
15
|
+
useNavigateWithTransition: () => mockNavigate,
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
describe('TransitionLink', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks()
|
|
21
|
+
// Reset console.warn mock
|
|
22
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders link with children', () => {
|
|
26
|
+
render(<TransitionLink to="/test-path">Click me</TransitionLink>)
|
|
27
|
+
|
|
28
|
+
const link = screen.getByRole('link', {name: /click me/i})
|
|
29
|
+
expect(link).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('sets href attribute for relative paths', () => {
|
|
33
|
+
render(<TransitionLink to="/test-path">Test Link</TransitionLink>)
|
|
34
|
+
|
|
35
|
+
const link = screen.getByRole('link')
|
|
36
|
+
expect(link).toHaveAttribute('href', '/test-path')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('navigates on click', async () => {
|
|
40
|
+
const user = userEvent.setup()
|
|
41
|
+
|
|
42
|
+
render(<TransitionLink to="/test-path">Navigate</TransitionLink>)
|
|
43
|
+
|
|
44
|
+
const link = screen.getByRole('link')
|
|
45
|
+
await user.click(link)
|
|
46
|
+
|
|
47
|
+
expect(mockNavigate).toHaveBeenCalledWith('/test-path')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('prevents default link behavior on click', async () => {
|
|
51
|
+
userEvent.setup()
|
|
52
|
+
const preventDefault = vi.fn()
|
|
53
|
+
|
|
54
|
+
render(<TransitionLink to="/test-path">Link</TransitionLink>)
|
|
55
|
+
|
|
56
|
+
const link = screen.getByRole('link')
|
|
57
|
+
|
|
58
|
+
// Manually create and dispatch event to test preventDefault
|
|
59
|
+
const clickEvent = new MouseEvent('click', {
|
|
60
|
+
bubbles: true,
|
|
61
|
+
cancelable: true,
|
|
62
|
+
})
|
|
63
|
+
Object.defineProperty(clickEvent, 'preventDefault', {
|
|
64
|
+
value: preventDefault,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
link.dispatchEvent(clickEvent)
|
|
68
|
+
|
|
69
|
+
expect(preventDefault).toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('calls custom onClick handler', async () => {
|
|
73
|
+
const user = userEvent.setup()
|
|
74
|
+
const handleClick = vi.fn()
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<TransitionLink to="/test-path" onClick={handleClick}>
|
|
78
|
+
Link
|
|
79
|
+
</TransitionLink>
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const link = screen.getByRole('link')
|
|
83
|
+
await user.click(link)
|
|
84
|
+
|
|
85
|
+
expect(handleClick).toHaveBeenCalled()
|
|
86
|
+
expect(mockNavigate).toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('does not navigate if onClick prevents default', async () => {
|
|
90
|
+
const user = userEvent.setup()
|
|
91
|
+
const handleClick = vi.fn(err => err.preventDefault())
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<TransitionLink to="/test-path" onClick={handleClick}>
|
|
95
|
+
Link
|
|
96
|
+
</TransitionLink>
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const link = screen.getByRole('link')
|
|
100
|
+
await user.click(link)
|
|
101
|
+
|
|
102
|
+
expect(handleClick).toHaveBeenCalled()
|
|
103
|
+
expect(mockNavigate).not.toHaveBeenCalled()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('warns about absolute URLs', () => {
|
|
107
|
+
const warnSpy = vi.spyOn(console, 'warn')
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
<TransitionLink to="https://example.com">External Link</TransitionLink>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
114
|
+
'TransitionLink: absolute URLs are not supported. Please update to a valid relative path.'
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('does not set href for absolute URLs', () => {
|
|
119
|
+
render(
|
|
120
|
+
<TransitionLink to="https://example.com">External Link</TransitionLink>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const link = screen.getByText('External Link')
|
|
124
|
+
expect(link).not.toHaveAttribute('href')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('forwards ref to anchor element', () => {
|
|
128
|
+
const ref = vi.fn()
|
|
129
|
+
|
|
130
|
+
render(
|
|
131
|
+
<TransitionLink to="/test" ref={ref}>
|
|
132
|
+
Link
|
|
133
|
+
</TransitionLink>
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(ref).toHaveBeenCalled()
|
|
137
|
+
expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLAnchorElement)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('passes additional props to anchor element', () => {
|
|
141
|
+
render(
|
|
142
|
+
<TransitionLink
|
|
143
|
+
to="/test"
|
|
144
|
+
className="custom-class"
|
|
145
|
+
data-testid="custom-link"
|
|
146
|
+
>
|
|
147
|
+
Link
|
|
148
|
+
</TransitionLink>
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const link = screen.getByRole('link')
|
|
152
|
+
expect(link).toHaveClass('custom-class')
|
|
153
|
+
expect(link).toHaveAttribute('data-testid', 'custom-link')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {render} from '../../test-utils'
|
|
6
|
+
|
|
7
|
+
import {Input} from './input'
|
|
8
|
+
|
|
9
|
+
describe('Input', () => {
|
|
10
|
+
it('accepts and forwards innerRef prop to input element', () => {
|
|
11
|
+
const ref = React.createRef<HTMLInputElement>()
|
|
12
|
+
|
|
13
|
+
const {container} = render(
|
|
14
|
+
<Input innerRef={ref} placeholder="Test input" />
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const inputElement = container.querySelector('input')
|
|
18
|
+
expect(inputElement).toBeTruthy()
|
|
19
|
+
expect(ref.current).toBe(inputElement)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -2,9 +2,18 @@ import * as React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import {cn} from '../../lib/utils'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// using the default ref doesn't seem to set the parent's ref object when mounted
|
|
6
|
+
// Since this is a shadCN component, we need to make sure to add back the innerRef prop,
|
|
7
|
+
// whenever the component is updated.
|
|
8
|
+
function Input({
|
|
9
|
+
innerRef,
|
|
10
|
+
className,
|
|
11
|
+
type,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<'input'> & {innerRef?: React.Ref<HTMLInputElement>}) {
|
|
6
14
|
return (
|
|
7
15
|
<input
|
|
16
|
+
ref={innerRef}
|
|
8
17
|
type={type}
|
|
9
18
|
data-slot="input"
|
|
10
19
|
className={cn(
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {renderHook, act} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useHandleAction} from '../../internal/useHandleAction'
|
|
5
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
6
|
+
import {useImageUpload} from '../storage/useImageUpload'
|
|
7
|
+
|
|
8
|
+
import {useCreateImageContent} from './useCreateImageContent'
|
|
9
|
+
|
|
10
|
+
// Mock the internal hooks and utilities
|
|
11
|
+
vi.mock('../../internal/useShopActions', () => ({
|
|
12
|
+
useShopActions: vi.fn(() => ({
|
|
13
|
+
createContent: vi.fn(),
|
|
14
|
+
})),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
vi.mock('../../internal/useHandleAction', () => ({
|
|
18
|
+
useHandleAction: vi.fn((action: any) => action),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
vi.mock('../storage/useImageUpload', () => ({
|
|
22
|
+
useImageUpload: vi.fn(() => ({
|
|
23
|
+
uploadImage: vi.fn(),
|
|
24
|
+
})),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
describe('useCreateImageContent', () => {
|
|
28
|
+
let mockCreateContent: ReturnType<typeof vi.fn>
|
|
29
|
+
let mockUploadImage: ReturnType<typeof vi.fn>
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks()
|
|
33
|
+
|
|
34
|
+
// Set up mock actions with proper implementations
|
|
35
|
+
mockCreateContent = vi.fn().mockResolvedValue({
|
|
36
|
+
data: {
|
|
37
|
+
publicId: 'content-123',
|
|
38
|
+
image: {
|
|
39
|
+
id: 'img-123',
|
|
40
|
+
url: 'https://example.com/content-image.jpg',
|
|
41
|
+
width: 800,
|
|
42
|
+
height: 600,
|
|
43
|
+
},
|
|
44
|
+
title: 'Test Content',
|
|
45
|
+
visibility: ['DISCOVERABLE'],
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
mockUploadImage = vi.fn().mockResolvedValue([
|
|
50
|
+
{
|
|
51
|
+
id: 'upload-123',
|
|
52
|
+
imageUrl: 'https://example.com/uploaded-image.jpg',
|
|
53
|
+
resourceUrl: 'https://example.com/resource/123',
|
|
54
|
+
},
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
// Update the mocks to return our mock actions
|
|
58
|
+
;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
59
|
+
createContent: mockCreateContent,
|
|
60
|
+
})
|
|
61
|
+
;(useImageUpload as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
62
|
+
uploadImage: mockUploadImage,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Make useHandleAction return the action directly
|
|
66
|
+
;(useHandleAction as ReturnType<typeof vi.fn>).mockImplementation(
|
|
67
|
+
(action: any) => action
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('Hook Structure', () => {
|
|
72
|
+
it('returns expected properties', () => {
|
|
73
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
74
|
+
|
|
75
|
+
expect(result.current).toHaveProperty('createImageContent')
|
|
76
|
+
expect(result.current).toHaveProperty('loading')
|
|
77
|
+
expect(typeof result.current.createImageContent).toBe('function')
|
|
78
|
+
expect(typeof result.current.loading).toBe('boolean')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('initializes with loading false', () => {
|
|
82
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
83
|
+
expect(result.current.loading).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('createImageContent', () => {
|
|
88
|
+
it('successfully creates image content', async () => {
|
|
89
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
90
|
+
|
|
91
|
+
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
92
|
+
const params = {
|
|
93
|
+
image: imageFile,
|
|
94
|
+
contentTitle: 'Test Content',
|
|
95
|
+
visibility: ['DISCOVERABLE'] as any,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await act(async () => {
|
|
99
|
+
const content = await result.current.createImageContent(params)
|
|
100
|
+
|
|
101
|
+
expect(content.data).toEqual({
|
|
102
|
+
publicId: 'content-123',
|
|
103
|
+
image: {
|
|
104
|
+
id: 'img-123',
|
|
105
|
+
url: 'https://example.com/content-image.jpg',
|
|
106
|
+
width: 800,
|
|
107
|
+
height: 600,
|
|
108
|
+
},
|
|
109
|
+
title: 'Test Content',
|
|
110
|
+
visibility: ['DISCOVERABLE'],
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Verify upload was called
|
|
115
|
+
expect(mockUploadImage).toHaveBeenCalledWith(imageFile)
|
|
116
|
+
expect(mockUploadImage).toHaveBeenCalledTimes(1)
|
|
117
|
+
|
|
118
|
+
// Verify createContent was called with correct params
|
|
119
|
+
expect(mockCreateContent).toHaveBeenCalledWith({
|
|
120
|
+
title: 'Test Content',
|
|
121
|
+
imageUrl: 'https://example.com/uploaded-image.jpg',
|
|
122
|
+
visibility: ['DISCOVERABLE'],
|
|
123
|
+
})
|
|
124
|
+
expect(mockCreateContent).toHaveBeenCalledTimes(1)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('sets loading state during operation', async () => {
|
|
128
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
129
|
+
|
|
130
|
+
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
131
|
+
const params = {
|
|
132
|
+
image: imageFile,
|
|
133
|
+
contentTitle: 'Test Content',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add a delay to the mock to test loading state
|
|
137
|
+
let resolveUpload: any
|
|
138
|
+
mockUploadImage.mockImplementation(
|
|
139
|
+
() =>
|
|
140
|
+
new Promise(resolve => {
|
|
141
|
+
resolveUpload = resolve
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Start the operation
|
|
146
|
+
let createPromise: Promise<any>
|
|
147
|
+
act(() => {
|
|
148
|
+
createPromise = result.current.createImageContent(params)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Check loading state is true while operation is in progress
|
|
152
|
+
expect(result.current.loading).toBe(true)
|
|
153
|
+
|
|
154
|
+
// Complete the upload
|
|
155
|
+
await act(async () => {
|
|
156
|
+
resolveUpload([
|
|
157
|
+
{
|
|
158
|
+
id: 'upload-123',
|
|
159
|
+
imageUrl: 'https://example.com/uploaded-image.jpg',
|
|
160
|
+
},
|
|
161
|
+
])
|
|
162
|
+
await createPromise
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Loading should be false after completion
|
|
166
|
+
expect(result.current.loading).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('throws error for missing file type', async () => {
|
|
170
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
171
|
+
|
|
172
|
+
// Create a file without a type
|
|
173
|
+
const imageFile = new File(['test'], 'test.jpg')
|
|
174
|
+
Object.defineProperty(imageFile, 'type', {value: undefined})
|
|
175
|
+
|
|
176
|
+
const params = {
|
|
177
|
+
image: imageFile,
|
|
178
|
+
contentTitle: 'Test Content',
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await act(async () => {
|
|
182
|
+
await expect(result.current.createImageContent(params)).rejects.toThrow(
|
|
183
|
+
'Unable to determine file type'
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
expect(mockUploadImage).not.toHaveBeenCalled()
|
|
188
|
+
expect(mockCreateContent).not.toHaveBeenCalled()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('throws error for non-image file type', async () => {
|
|
192
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
193
|
+
|
|
194
|
+
const textFile = new File(['test'], 'test.txt', {type: 'text/plain'})
|
|
195
|
+
const params = {
|
|
196
|
+
image: textFile,
|
|
197
|
+
contentTitle: 'Test Content',
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await act(async () => {
|
|
201
|
+
await expect(result.current.createImageContent(params)).rejects.toThrow(
|
|
202
|
+
'Invalid file type: must be an image'
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
expect(mockUploadImage).not.toHaveBeenCalled()
|
|
207
|
+
expect(mockCreateContent).not.toHaveBeenCalled()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('throws error when image upload fails', async () => {
|
|
211
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
212
|
+
|
|
213
|
+
// Mock upload to return no URL
|
|
214
|
+
mockUploadImage.mockResolvedValue([
|
|
215
|
+
{
|
|
216
|
+
id: 'upload-123',
|
|
217
|
+
imageUrl: undefined,
|
|
218
|
+
resourceUrl: 'https://example.com/resource/123',
|
|
219
|
+
},
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
223
|
+
const params = {
|
|
224
|
+
image: imageFile,
|
|
225
|
+
contentTitle: 'Test Content',
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await act(async () => {
|
|
229
|
+
await expect(result.current.createImageContent(params)).rejects.toThrow(
|
|
230
|
+
'Image upload failed'
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
expect(mockUploadImage).toHaveBeenCalledWith(imageFile)
|
|
235
|
+
expect(mockCreateContent).not.toHaveBeenCalled()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('handles content creation with null visibility', async () => {
|
|
239
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
240
|
+
|
|
241
|
+
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
242
|
+
const params = {
|
|
243
|
+
image: imageFile,
|
|
244
|
+
contentTitle: 'Test Content',
|
|
245
|
+
visibility: null,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await act(async () => {
|
|
249
|
+
await result.current.createImageContent(params)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
expect(mockCreateContent).toHaveBeenCalledWith({
|
|
253
|
+
title: 'Test Content',
|
|
254
|
+
imageUrl: 'https://example.com/uploaded-image.jpg',
|
|
255
|
+
visibility: null,
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('returns user errors from content creation', async () => {
|
|
260
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
261
|
+
|
|
262
|
+
mockCreateContent.mockResolvedValue({
|
|
263
|
+
data: {
|
|
264
|
+
publicId: 'content-123',
|
|
265
|
+
title: 'Test Content',
|
|
266
|
+
},
|
|
267
|
+
userErrors: [
|
|
268
|
+
{
|
|
269
|
+
field: 'visibility',
|
|
270
|
+
message: 'Invalid visibility',
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
276
|
+
const params = {
|
|
277
|
+
image: imageFile,
|
|
278
|
+
contentTitle: 'Test Content',
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await act(async () => {
|
|
282
|
+
const contentResult = await result.current.createImageContent(params)
|
|
283
|
+
|
|
284
|
+
expect(contentResult.userErrors).toEqual([
|
|
285
|
+
{
|
|
286
|
+
field: 'visibility',
|
|
287
|
+
message: 'Invalid visibility',
|
|
288
|
+
},
|
|
289
|
+
])
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe('Error Handling', () => {
|
|
295
|
+
it('handles upload error properly', async () => {
|
|
296
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
297
|
+
|
|
298
|
+
mockUploadImage.mockRejectedValue(new Error('Upload failed'))
|
|
299
|
+
|
|
300
|
+
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
301
|
+
const params = {
|
|
302
|
+
image: imageFile,
|
|
303
|
+
contentTitle: 'Test Content',
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check that error is thrown
|
|
307
|
+
await act(async () => {
|
|
308
|
+
await expect(result.current.createImageContent(params)).rejects.toThrow(
|
|
309
|
+
'Upload failed'
|
|
310
|
+
)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Loading state is only managed during successful operations
|
|
314
|
+
// The hook doesn't reset loading on error since it's controlled by the consumer
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('handles content creation error properly', async () => {
|
|
318
|
+
const {result} = renderHook(() => useCreateImageContent())
|
|
319
|
+
|
|
320
|
+
mockCreateContent.mockRejectedValue(new Error('Creation failed'))
|
|
321
|
+
|
|
322
|
+
const imageFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
323
|
+
const params = {
|
|
324
|
+
image: imageFile,
|
|
325
|
+
contentTitle: 'Test Content',
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check that error is thrown
|
|
329
|
+
await act(async () => {
|
|
330
|
+
await expect(result.current.createImageContent(params)).rejects.toThrow(
|
|
331
|
+
'Creation failed'
|
|
332
|
+
)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// Loading state is only managed during successful operations
|
|
336
|
+
// The hook doesn't reset loading on error since it's controlled by the consumer
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe('Stability', () => {
|
|
341
|
+
it('maintains function reference stability across renders', () => {
|
|
342
|
+
const {result, rerender} = renderHook(() => useCreateImageContent())
|
|
343
|
+
|
|
344
|
+
const firstRender = result.current.createImageContent
|
|
345
|
+
rerender()
|
|
346
|
+
const secondRender = result.current.createImageContent
|
|
347
|
+
|
|
348
|
+
// Function should maintain reference equality
|
|
349
|
+
expect(firstRender).toBe(secondRender)
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
})
|
package/src/hooks/index.ts
CHANGED