@shopify/shop-minis-react 0.0.34 → 0.0.36
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/index2.js +4 -4
- package/dist/_virtual/index3.js +4 -4
- 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/hooks/util/useShare.js +7 -6
- package/dist/hooks/util/useShare.js.map +1 -1
- package/dist/index.js +228 -222
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +17 -10
- package/dist/mocks.js.map +1 -1
- package/dist/shop-minis-platform/src/types/share.js +5 -0
- package/dist/shop-minis-platform/src/types/share.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
- package/dist/utils/colors.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/hooks/util/useShare.ts +13 -3
- package/src/internal/useHandleAction.test.ts +265 -0
- package/src/internal/useShopActionsDataFetching.test.ts +465 -0
- package/src/mocks.ts +7 -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,467 @@
|
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
fireEvent,
|
|
5
|
+
renderHook,
|
|
6
|
+
act,
|
|
7
|
+
} from '@testing-library/react'
|
|
8
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
9
|
+
|
|
10
|
+
import {ImagePickerProvider, useImagePickerContext} from './ImagePickerProvider'
|
|
11
|
+
|
|
12
|
+
describe('ImagePickerProvider', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('Context', () => {
|
|
18
|
+
it('throws error when used outside of provider', () => {
|
|
19
|
+
// Silence console.error for this test
|
|
20
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
21
|
+
|
|
22
|
+
expect(() => {
|
|
23
|
+
renderHook(() => useImagePickerContext())
|
|
24
|
+
}).toThrow(
|
|
25
|
+
'useImagePickerContext must be used within an ImagePickerProvider'
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
consoleSpy.mockRestore()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('provides context value when used within provider', () => {
|
|
32
|
+
const {result} = renderHook(() => useImagePickerContext(), {
|
|
33
|
+
wrapper: ({children}) => (
|
|
34
|
+
<ImagePickerProvider>{children}</ImagePickerProvider>
|
|
35
|
+
),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(result.current).toHaveProperty('openCamera')
|
|
39
|
+
expect(result.current).toHaveProperty('openGallery')
|
|
40
|
+
expect(typeof result.current.openCamera).toBe('function')
|
|
41
|
+
expect(typeof result.current.openGallery).toBe('function')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('Hidden Inputs', () => {
|
|
46
|
+
it('renders hidden file inputs', () => {
|
|
47
|
+
const {container} = render(
|
|
48
|
+
<ImagePickerProvider>
|
|
49
|
+
<div>Test Content</div>
|
|
50
|
+
</ImagePickerProvider>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const inputs = container.querySelectorAll('input[type="file"]')
|
|
54
|
+
expect(inputs).toHaveLength(3)
|
|
55
|
+
|
|
56
|
+
// Gallery input
|
|
57
|
+
const galleryInput = inputs[0]
|
|
58
|
+
expect(galleryInput).toHaveAttribute('accept', 'image/*')
|
|
59
|
+
expect(galleryInput).not.toHaveAttribute('capture')
|
|
60
|
+
expect(galleryInput).toHaveStyle({display: 'none'})
|
|
61
|
+
|
|
62
|
+
// Front camera input
|
|
63
|
+
const frontCameraInput = inputs[1]
|
|
64
|
+
expect(frontCameraInput).toHaveAttribute('accept', 'image/*')
|
|
65
|
+
expect(frontCameraInput).toHaveAttribute('capture', 'user')
|
|
66
|
+
|
|
67
|
+
// Back camera input
|
|
68
|
+
const backCameraInput = inputs[2]
|
|
69
|
+
expect(backCameraInput).toHaveAttribute('accept', 'image/*')
|
|
70
|
+
expect(backCameraInput).toHaveAttribute('capture', 'environment')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('openGallery', () => {
|
|
75
|
+
it('triggers gallery input click and resolves with selected file', async () => {
|
|
76
|
+
const TestComponent = () => {
|
|
77
|
+
const {openGallery} = useImagePickerContext()
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={() =>
|
|
83
|
+
openGallery()
|
|
84
|
+
.then(file => {
|
|
85
|
+
const span = document.createElement('span')
|
|
86
|
+
span.textContent = file.name
|
|
87
|
+
span.setAttribute('data-testid', 'selected-file')
|
|
88
|
+
document.body.appendChild(span)
|
|
89
|
+
})
|
|
90
|
+
.catch(() => {
|
|
91
|
+
// Ignore errors from cleanup
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
Open Gallery
|
|
96
|
+
</button>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const {container} = render(
|
|
101
|
+
<ImagePickerProvider>
|
|
102
|
+
<TestComponent />
|
|
103
|
+
</ImagePickerProvider>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const galleryInput = container.querySelector(
|
|
107
|
+
'input[type="file"]:not([capture])'
|
|
108
|
+
) as HTMLInputElement
|
|
109
|
+
const clickSpy = vi.spyOn(galleryInput, 'click')
|
|
110
|
+
|
|
111
|
+
// Click the button to open gallery
|
|
112
|
+
const button = screen.getByText('Open Gallery')
|
|
113
|
+
fireEvent.click(button)
|
|
114
|
+
|
|
115
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
116
|
+
|
|
117
|
+
// Simulate file selection
|
|
118
|
+
const file = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
119
|
+
Object.defineProperty(galleryInput, 'files', {
|
|
120
|
+
value: [file],
|
|
121
|
+
configurable: true,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await act(async () => {
|
|
125
|
+
fireEvent.change(galleryInput)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Check that the promise resolved with the file
|
|
129
|
+
await vi.waitFor(() => {
|
|
130
|
+
const selectedFile = document.querySelector(
|
|
131
|
+
'[data-testid="selected-file"]'
|
|
132
|
+
)
|
|
133
|
+
expect(selectedFile?.textContent).toBe('test.jpg')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('handles cancel event', async () => {
|
|
138
|
+
const TestComponent = () => {
|
|
139
|
+
const {openGallery} = useImagePickerContext()
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() =>
|
|
145
|
+
openGallery().catch(error => {
|
|
146
|
+
const span = document.createElement('span')
|
|
147
|
+
span.textContent = error.message
|
|
148
|
+
span.setAttribute('data-testid', 'cancel-message')
|
|
149
|
+
document.body.appendChild(span)
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
Open Gallery
|
|
154
|
+
</button>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const {container} = render(
|
|
159
|
+
<ImagePickerProvider>
|
|
160
|
+
<TestComponent />
|
|
161
|
+
</ImagePickerProvider>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const galleryInput = container.querySelector(
|
|
165
|
+
'input[type="file"]:not([capture])'
|
|
166
|
+
) as HTMLInputElement
|
|
167
|
+
|
|
168
|
+
const button = screen.getByText('Open Gallery')
|
|
169
|
+
fireEvent.click(button)
|
|
170
|
+
|
|
171
|
+
// Simulate cancel event
|
|
172
|
+
await act(async () => {
|
|
173
|
+
const cancelEvent = new Event('cancel', {bubbles: true})
|
|
174
|
+
galleryInput.dispatchEvent(cancelEvent)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await vi.waitFor(() => {
|
|
178
|
+
const cancelMessage = document.querySelector(
|
|
179
|
+
'[data-testid="cancel-message"]'
|
|
180
|
+
)
|
|
181
|
+
expect(cancelMessage?.textContent).toBe('User cancelled file selection')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('openCamera', () => {
|
|
187
|
+
it('triggers back camera input click by default', async () => {
|
|
188
|
+
const TestComponent = () => {
|
|
189
|
+
const {openCamera} = useImagePickerContext()
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() =>
|
|
195
|
+
openCamera().catch(() => {
|
|
196
|
+
// Ignore errors from cleanup
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
Open Camera
|
|
201
|
+
</button>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const {container} = render(
|
|
206
|
+
<ImagePickerProvider>
|
|
207
|
+
<TestComponent />
|
|
208
|
+
</ImagePickerProvider>
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const backCameraInput = container.querySelector(
|
|
212
|
+
'input[capture="environment"]'
|
|
213
|
+
) as HTMLInputElement
|
|
214
|
+
const clickSpy = vi.spyOn(backCameraInput, 'click')
|
|
215
|
+
|
|
216
|
+
const button = screen.getByText('Open Camera')
|
|
217
|
+
fireEvent.click(button)
|
|
218
|
+
|
|
219
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('triggers front camera input click when specified', async () => {
|
|
223
|
+
const TestComponent = () => {
|
|
224
|
+
const {openCamera} = useImagePickerContext()
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
onClick={() =>
|
|
230
|
+
openCamera('front').catch(() => {
|
|
231
|
+
// Ignore errors from cleanup
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
>
|
|
235
|
+
Open Front Camera
|
|
236
|
+
</button>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const {container} = render(
|
|
241
|
+
<ImagePickerProvider>
|
|
242
|
+
<TestComponent />
|
|
243
|
+
</ImagePickerProvider>
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
const frontCameraInput = container.querySelector(
|
|
247
|
+
'input[capture="user"]'
|
|
248
|
+
) as HTMLInputElement
|
|
249
|
+
const clickSpy = vi.spyOn(frontCameraInput, 'click')
|
|
250
|
+
|
|
251
|
+
const button = screen.getByText('Open Front Camera')
|
|
252
|
+
fireEvent.click(button)
|
|
253
|
+
|
|
254
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('resolves with selected file from camera', async () => {
|
|
258
|
+
const TestComponent = () => {
|
|
259
|
+
const {openCamera} = useImagePickerContext()
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
onClick={() =>
|
|
265
|
+
openCamera()
|
|
266
|
+
.then(file => {
|
|
267
|
+
const span = document.createElement('span')
|
|
268
|
+
span.textContent = file.name
|
|
269
|
+
span.setAttribute('data-testid', 'camera-file')
|
|
270
|
+
document.body.appendChild(span)
|
|
271
|
+
})
|
|
272
|
+
.catch(() => {
|
|
273
|
+
// Ignore errors from cleanup
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
>
|
|
277
|
+
Open Camera
|
|
278
|
+
</button>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const {container} = render(
|
|
283
|
+
<ImagePickerProvider>
|
|
284
|
+
<TestComponent />
|
|
285
|
+
</ImagePickerProvider>
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
const cameraInput = container.querySelector(
|
|
289
|
+
'input[capture="environment"]'
|
|
290
|
+
) as HTMLInputElement
|
|
291
|
+
|
|
292
|
+
const button = screen.getByText('Open Camera')
|
|
293
|
+
fireEvent.click(button)
|
|
294
|
+
|
|
295
|
+
// Simulate file capture
|
|
296
|
+
const file = new File(['photo'], 'photo.jpg', {type: 'image/jpeg'})
|
|
297
|
+
Object.defineProperty(cameraInput, 'files', {
|
|
298
|
+
value: [file],
|
|
299
|
+
configurable: true,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
await act(async () => {
|
|
303
|
+
fireEvent.change(cameraInput)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
await vi.waitFor(() => {
|
|
307
|
+
const cameraFile = document.querySelector('[data-testid="camera-file"]')
|
|
308
|
+
expect(cameraFile?.textContent).toBe('photo.jpg')
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('handles cancel event for camera', async () => {
|
|
313
|
+
const TestComponent = () => {
|
|
314
|
+
const {openCamera} = useImagePickerContext()
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
onClick={() =>
|
|
320
|
+
openCamera().catch(error => {
|
|
321
|
+
const span = document.createElement('span')
|
|
322
|
+
span.textContent = error.message
|
|
323
|
+
span.setAttribute('data-testid', 'camera-cancel')
|
|
324
|
+
document.body.appendChild(span)
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
>
|
|
328
|
+
Open Camera
|
|
329
|
+
</button>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const {container} = render(
|
|
334
|
+
<ImagePickerProvider>
|
|
335
|
+
<TestComponent />
|
|
336
|
+
</ImagePickerProvider>
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
const cameraInput = container.querySelector(
|
|
340
|
+
'input[capture="environment"]'
|
|
341
|
+
) as HTMLInputElement
|
|
342
|
+
|
|
343
|
+
const button = screen.getByText('Open Camera')
|
|
344
|
+
fireEvent.click(button)
|
|
345
|
+
|
|
346
|
+
// Simulate cancel event
|
|
347
|
+
await act(async () => {
|
|
348
|
+
const cancelEvent = new Event('cancel', {bubbles: true})
|
|
349
|
+
cameraInput.dispatchEvent(cancelEvent)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
await vi.waitFor(() => {
|
|
353
|
+
const cancelMessage = document.querySelector(
|
|
354
|
+
'[data-testid="camera-cancel"]'
|
|
355
|
+
)
|
|
356
|
+
expect(cancelMessage?.textContent).toBe('User cancelled camera')
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('Multiple Picker Handling', () => {
|
|
362
|
+
it('rejects previous promise when new picker is opened', async () => {
|
|
363
|
+
const TestComponent = () => {
|
|
364
|
+
const {openCamera, openGallery} = useImagePickerContext()
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<>
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
onClick={() =>
|
|
371
|
+
openGallery().catch(error => {
|
|
372
|
+
const span = document.createElement('span')
|
|
373
|
+
span.textContent = error.message
|
|
374
|
+
span.setAttribute('data-testid', 'gallery-error')
|
|
375
|
+
document.body.appendChild(span)
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
>
|
|
379
|
+
Open Gallery
|
|
380
|
+
</button>
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
onClick={() =>
|
|
384
|
+
openCamera().catch(() => {
|
|
385
|
+
// Ignore errors from cleanup
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
>
|
|
389
|
+
Open Camera
|
|
390
|
+
</button>
|
|
391
|
+
</>
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
render(
|
|
396
|
+
<ImagePickerProvider>
|
|
397
|
+
<TestComponent />
|
|
398
|
+
</ImagePickerProvider>
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
// Open gallery first
|
|
402
|
+
const galleryButton = screen.getByText('Open Gallery')
|
|
403
|
+
fireEvent.click(galleryButton)
|
|
404
|
+
|
|
405
|
+
// Then immediately open camera
|
|
406
|
+
const cameraButton = screen.getByText('Open Camera')
|
|
407
|
+
fireEvent.click(cameraButton)
|
|
408
|
+
|
|
409
|
+
await vi.waitFor(() => {
|
|
410
|
+
const errorMessage = document.querySelector(
|
|
411
|
+
'[data-testid="gallery-error"]'
|
|
412
|
+
)
|
|
413
|
+
expect(errorMessage?.textContent).toBe(
|
|
414
|
+
'New file picker opened before previous completed'
|
|
415
|
+
)
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
describe('Cleanup', () => {
|
|
421
|
+
it('clears input value after file selection', async () => {
|
|
422
|
+
const TestComponent = () => {
|
|
423
|
+
const {openGallery} = useImagePickerContext()
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
onClick={() =>
|
|
429
|
+
openGallery().catch(() => {
|
|
430
|
+
// Ignore errors from cleanup
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
>
|
|
434
|
+
Open Gallery
|
|
435
|
+
</button>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const {container} = render(
|
|
440
|
+
<ImagePickerProvider>
|
|
441
|
+
<TestComponent />
|
|
442
|
+
</ImagePickerProvider>
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
const galleryInput = container.querySelector(
|
|
446
|
+
'input[type="file"]:not([capture])'
|
|
447
|
+
) as HTMLInputElement
|
|
448
|
+
|
|
449
|
+
const button = screen.getByText('Open Gallery')
|
|
450
|
+
fireEvent.click(button)
|
|
451
|
+
|
|
452
|
+
// Set file and trigger change
|
|
453
|
+
const file = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
454
|
+
Object.defineProperty(galleryInput, 'files', {
|
|
455
|
+
value: [file],
|
|
456
|
+
configurable: true,
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
await act(async () => {
|
|
460
|
+
fireEvent.change(galleryInput)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// Check that input value was cleared
|
|
464
|
+
expect(galleryInput.value).toBe('')
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
})
|
|
@@ -36,7 +36,7 @@ injectMocks()
|
|
|
36
36
|
export const Single: Story = {
|
|
37
37
|
decorators: [
|
|
38
38
|
Story => (
|
|
39
|
-
<div style={{maxWidth:
|
|
39
|
+
<div style={{maxWidth: 150}}>
|
|
40
40
|
<Story />
|
|
41
41
|
</div>
|
|
42
42
|
),
|
|
@@ -69,7 +69,7 @@ export const Single: Story = {
|
|
|
69
69
|
|
|
70
70
|
featuredImage: {
|
|
71
71
|
url: 'https://cdn.shopify.com/static/sample-images/teapot.jpg',
|
|
72
|
-
altText: '
|
|
72
|
+
altText: 'Teapot',
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
},
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {fn} from 'storybook/test'
|
|
2
|
+
|
|
3
|
+
import {TextInput} from '../components'
|
|
4
|
+
|
|
5
|
+
import type {Meta, StoryObj} from '@storybook/react-vite'
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Atoms/TextInput',
|
|
9
|
+
component: TextInput,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
},
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
} satisfies Meta<typeof TextInput>
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
type Story = StoryObj<typeof meta>
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
placeholder: 'Search...',
|
|
22
|
+
onFocus: fn(),
|
|
23
|
+
onBlur: fn(),
|
|
24
|
+
onChange: fn(),
|
|
25
|
+
},
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import {cleanup} from '@testing-library/react'
|
|
3
|
+
import {afterEach, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
// Clean up after each test to prevent test pollution
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// Mock window.matchMedia if not available (for components using media queries)
|
|
11
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
12
|
+
writable: true,
|
|
13
|
+
value: vi.fn().mockImplementation((query: string) => ({
|
|
14
|
+
matches: false,
|
|
15
|
+
media: query,
|
|
16
|
+
onchange: null,
|
|
17
|
+
addListener: vi.fn(), // deprecated
|
|
18
|
+
removeListener: vi.fn(), // deprecated
|
|
19
|
+
addEventListener: vi.fn(),
|
|
20
|
+
removeEventListener: vi.fn(),
|
|
21
|
+
dispatchEvent: vi.fn(),
|
|
22
|
+
})),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Mock IntersectionObserver (used by List component)
|
|
26
|
+
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
|
27
|
+
observe: vi.fn(),
|
|
28
|
+
unobserve: vi.fn(),
|
|
29
|
+
disconnect: vi.fn(),
|
|
30
|
+
root: null,
|
|
31
|
+
rootMargin: '',
|
|
32
|
+
thresholds: [],
|
|
33
|
+
takeRecords: vi.fn(),
|
|
34
|
+
}))
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
type RenderOptions,
|
|
6
|
+
type RenderResult,
|
|
7
|
+
} from '@testing-library/react'
|
|
8
|
+
import {vi} from 'vitest'
|
|
9
|
+
|
|
10
|
+
import {createProduct, createShop} from './mocks'
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
Product,
|
|
14
|
+
ProductVariant,
|
|
15
|
+
Shop,
|
|
16
|
+
ProductImage,
|
|
17
|
+
Money,
|
|
18
|
+
} from '@shopify/shop-minis-platform'
|
|
19
|
+
|
|
20
|
+
// Create spyable mock SDK functions
|
|
21
|
+
const createSpyableMockSDK = () => {
|
|
22
|
+
return {
|
|
23
|
+
navigateToProduct: vi.fn(),
|
|
24
|
+
navigateToShop: vi.fn(),
|
|
25
|
+
saveProduct: vi.fn().mockResolvedValue({ok: true, data: undefined}),
|
|
26
|
+
unsaveProduct: vi.fn().mockResolvedValue({ok: true, data: undefined}),
|
|
27
|
+
favorite: vi.fn().mockResolvedValue({ok: true, data: undefined}),
|
|
28
|
+
unfavorite: vi.fn().mockResolvedValue({ok: true, data: undefined}),
|
|
29
|
+
followShop: vi.fn().mockResolvedValue({ok: true, data: true}),
|
|
30
|
+
unfollowShop: vi.fn().mockResolvedValue({ok: true, data: false}),
|
|
31
|
+
buyProduct: vi.fn(),
|
|
32
|
+
buyProducts: vi.fn(),
|
|
33
|
+
addToCart: vi.fn(),
|
|
34
|
+
getProduct: vi.fn(),
|
|
35
|
+
getProducts: vi.fn(),
|
|
36
|
+
getProductSearch: vi.fn(),
|
|
37
|
+
getRecommendedProducts: vi.fn(),
|
|
38
|
+
getPopularProducts: vi.fn(),
|
|
39
|
+
getSavedProducts: vi.fn(),
|
|
40
|
+
getRecentProducts: vi.fn(),
|
|
41
|
+
getCuratedProducts: vi.fn(),
|
|
42
|
+
share: vi.fn(),
|
|
43
|
+
closeMini: vi.fn(),
|
|
44
|
+
showErrorScreen: vi.fn(),
|
|
45
|
+
showErrorToast: vi.fn(),
|
|
46
|
+
getAccountInformation: vi.fn(),
|
|
47
|
+
getPersistedItem: vi.fn(),
|
|
48
|
+
setPersistedItem: vi.fn(),
|
|
49
|
+
removePersistedItem: vi.fn(),
|
|
50
|
+
clearPersistedItems: vi.fn(),
|
|
51
|
+
} as any
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Mock window.minisSDK for tests
|
|
55
|
+
export const mockMinisSDK = createSpyableMockSDK()
|
|
56
|
+
|
|
57
|
+
// Setup minisSDK mock globally
|
|
58
|
+
if (typeof window !== 'undefined') {
|
|
59
|
+
;(window as any).minisSDK = mockMinisSDK
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Custom render with providers if needed
|
|
63
|
+
export function renderWithProviders(
|
|
64
|
+
ui: React.ReactElement,
|
|
65
|
+
options?: Omit<RenderOptions, 'wrapper'>
|
|
66
|
+
): RenderResult {
|
|
67
|
+
return render(ui, {
|
|
68
|
+
...options,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Re-export mock factories from mocks.ts with backwards compatibility
|
|
73
|
+
export const mockProduct = (overrides: Partial<Product> = {}): Product => {
|
|
74
|
+
const baseProduct = createProduct(
|
|
75
|
+
overrides.id || 'product-1',
|
|
76
|
+
overrides.title || 'Test Product',
|
|
77
|
+
overrides.price?.amount || '99.99',
|
|
78
|
+
overrides.compareAtPrice?.amount
|
|
79
|
+
)
|
|
80
|
+
return {
|
|
81
|
+
...baseProduct,
|
|
82
|
+
...overrides,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const mockShop = (overrides: Partial<Shop> = {}): Shop => {
|
|
87
|
+
const baseShop = createShop(
|
|
88
|
+
overrides.id || 'shop-1',
|
|
89
|
+
overrides.name || 'Test Shop'
|
|
90
|
+
)
|
|
91
|
+
return {
|
|
92
|
+
...baseShop,
|
|
93
|
+
...overrides,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Helper to create multiple products
|
|
98
|
+
export const mockProducts = (count = 5): Product[] => {
|
|
99
|
+
return Array.from({length: count}, (_, i) =>
|
|
100
|
+
createProduct(
|
|
101
|
+
`product-${i + 1}`,
|
|
102
|
+
`Test Product ${i + 1}`,
|
|
103
|
+
`${(i + 1) * 50}.00`
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Export commonly used mock data helpers for backwards compatibility
|
|
109
|
+
export const mockMoney = (amount = '29.99', currencyCode = 'USD'): Money => ({
|
|
110
|
+
amount,
|
|
111
|
+
currencyCode: currencyCode as any,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export const mockProductImage = (
|
|
115
|
+
overrides: Partial<ProductImage> = {}
|
|
116
|
+
): ProductImage => ({
|
|
117
|
+
url: 'https://example.com/product-image.jpg',
|
|
118
|
+
altText: 'Product image',
|
|
119
|
+
width: 1000,
|
|
120
|
+
height: 1000,
|
|
121
|
+
sensitive: false,
|
|
122
|
+
thumbhash: 'someThumbhash',
|
|
123
|
+
...overrides,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
export const mockProductVariant = (
|
|
127
|
+
overrides: Partial<ProductVariant> = {}
|
|
128
|
+
): ProductVariant => ({
|
|
129
|
+
id: 'variant-1',
|
|
130
|
+
isFavorited: false,
|
|
131
|
+
price: mockMoney('29.99', 'USD'),
|
|
132
|
+
compareAtPrice: mockMoney('39.99', 'USD'),
|
|
133
|
+
image: mockProductImage(),
|
|
134
|
+
...overrides,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Helper to wait for async updates
|
|
138
|
+
export const waitForAsync = () => new Promise(resolve => setTimeout(resolve, 0))
|
|
139
|
+
|
|
140
|
+
// Common test assertions for accessibility
|
|
141
|
+
export const expectToBeAccessible = (element: HTMLElement) => {
|
|
142
|
+
// Check for basic accessibility attributes
|
|
143
|
+
const role = element.getAttribute('role')
|
|
144
|
+
const ariaLabel = element.getAttribute('aria-label')
|
|
145
|
+
const ariaLabelledBy = element.getAttribute('aria-labelledby')
|
|
146
|
+
|
|
147
|
+
// At least one of these should be present for interactive elements
|
|
148
|
+
if (element.tagName === 'BUTTON' || element.onclick) {
|
|
149
|
+
const hasAccessibility = Boolean(
|
|
150
|
+
role || ariaLabel || ariaLabelledBy || element.textContent?.trim()
|
|
151
|
+
)
|
|
152
|
+
expect(hasAccessibility).toBe(true)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Reset all mocks
|
|
157
|
+
export const resetAllMocks = () => {
|
|
158
|
+
Object.values(mockMinisSDK).forEach(mock => {
|
|
159
|
+
if (typeof mock === 'function' && 'mockReset' in mock) {
|
|
160
|
+
;(mock as any).mockReset()
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Export everything from testing library for convenience
|
|
166
|
+
export * from '@testing-library/react'
|
|
167
|
+
export {default as userEvent} from '@testing-library/user-event'
|