@shopify/shop-minis-react 0.3.4 → 0.4.1
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/index3.js +2 -5
- package/dist/_virtual/index3.js.map +1 -1
- package/dist/_virtual/index4.js +5 -2
- package/dist/_virtual/index4.js.map +1 -1
- package/dist/components/MinisContainer.js +11 -10
- package/dist/components/MinisContainer.js.map +1 -1
- package/dist/hooks/content/useContent.js +12 -18
- package/dist/hooks/content/useContent.js.map +1 -1
- package/dist/hooks/product/useCuratedProducts.js +9 -11
- package/dist/hooks/product/useCuratedProducts.js.map +1 -1
- package/dist/hooks/product/usePopularProducts.js +9 -11
- package/dist/hooks/product/usePopularProducts.js.map +1 -1
- package/dist/hooks/product/useProduct.js +11 -17
- package/dist/hooks/product/useProduct.js.map +1 -1
- package/dist/hooks/product/useProductList.js +10 -21
- package/dist/hooks/product/useProductList.js.map +1 -1
- package/dist/hooks/product/useProductLists.js +11 -13
- package/dist/hooks/product/useProductLists.js.map +1 -1
- package/dist/hooks/product/useProductMedia.js +12 -18
- package/dist/hooks/product/useProductMedia.js.map +1 -1
- package/dist/hooks/product/useProductSearch.js +34 -27
- package/dist/hooks/product/useProductSearch.js.map +1 -1
- package/dist/hooks/product/useProductVariants.js +11 -14
- package/dist/hooks/product/useProductVariants.js.map +1 -1
- package/dist/hooks/product/useProducts.js +12 -11
- package/dist/hooks/product/useProducts.js.map +1 -1
- package/dist/hooks/product/useRecommendedProducts.js +11 -13
- package/dist/hooks/product/useRecommendedProducts.js.map +1 -1
- package/dist/hooks/shop/useRecommendedShops.js +11 -13
- package/dist/hooks/shop/useRecommendedShops.js.map +1 -1
- package/dist/hooks/shop/useShop.js +12 -11
- package/dist/hooks/shop/useShop.js.map +1 -1
- package/dist/hooks/user/useBuyerAttributes.js +8 -10
- package/dist/hooks/user/useBuyerAttributes.js.map +1 -1
- package/dist/hooks/user/useCurrentUser.js +7 -9
- package/dist/hooks/user/useCurrentUser.js.map +1 -1
- package/dist/hooks/user/useFollowedShops.js +11 -14
- package/dist/hooks/user/useFollowedShops.js.map +1 -1
- package/dist/hooks/user/useOrders.js +7 -9
- package/dist/hooks/user/useOrders.js.map +1 -1
- package/dist/hooks/user/useRecentProducts.js +11 -13
- package/dist/hooks/user/useRecentProducts.js.map +1 -1
- package/dist/hooks/user/useRecentShops.js +10 -13
- package/dist/hooks/user/useRecentShops.js.map +1 -1
- package/dist/hooks/user/useSavedProducts.js +10 -13
- package/dist/hooks/user/useSavedProducts.js.map +1 -1
- package/dist/hooks/util/useImagePicker.js +13 -6
- package/dist/hooks/util/useImagePicker.js.map +1 -1
- package/dist/internal/reactQuery/MinisQueryProvider.js +11 -0
- package/dist/internal/reactQuery/MinisQueryProvider.js.map +1 -0
- package/dist/internal/reactQuery/queryClient.js +33 -0
- package/dist/internal/reactQuery/queryClient.js.map +1 -0
- package/dist/internal/reactQuery/useShopActionInfiniteQuery.js +52 -0
- package/dist/internal/reactQuery/useShopActionInfiniteQuery.js.map +1 -0
- package/dist/internal/reactQuery/useShopActionQuery.js +37 -0
- package/dist/internal/reactQuery/useShopActionQuery.js.map +1 -0
- package/dist/internal/utils/resizeImage.js +61 -0
- package/dist/internal/utils/resizeImage.js.map +1 -0
- package/dist/providers/ImagePickerProvider.js +123 -102
- package/dist/providers/ImagePickerProvider.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/focusManager.js +45 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/focusManager.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryBehavior.js +89 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryBehavior.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryObserver.js +55 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/infiniteQueryObserver.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutation.js +198 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutation.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutationCache.js +99 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/mutationCache.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/notifyManager.js +67 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/notifyManager.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/onlineManager.js +39 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/onlineManager.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/query.js +299 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/query.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryCache.js +80 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryCache.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryClient.js +215 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryClient.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryObserver.js +300 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/queryObserver.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/removable.js +25 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/removable.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/retryer.js +76 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/retryer.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/subscribable.js +21 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/subscribable.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/thenable.js +26 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/thenable.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/utils.js +176 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_query-core@5.86.0/node_modules/@tanstack/query-core/build/modern/utils.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/IsRestoringProvider.js +7 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/IsRestoringProvider.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js +17 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryErrorResetBoundary.js +19 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/QueryErrorResetBoundary.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/errorBoundaryUtils.js +21 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/errorBoundaryUtils.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/suspense.js +18 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/suspense.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useBaseQuery.js +64 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useBaseQuery.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useInfiniteQuery.js +13 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useInfiniteQuery.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useQuery.js +9 -0
- package/dist/shop-minis-react/node_modules/.pnpm/@tanstack_react-query@5.86.0_react@19.1.0/node_modules/@tanstack/react-query/build/modern/useQuery.js.map +1 -0
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
- package/package.json +2 -7
- package/src/components/MinisContainer.tsx +6 -3
- package/src/hooks/content/useContent.ts +6 -17
- package/src/hooks/product/useCuratedProducts.ts +4 -6
- package/src/hooks/product/usePopularProducts.ts +4 -6
- package/src/hooks/product/useProduct.ts +6 -17
- package/src/hooks/product/useProductList.ts +4 -19
- package/src/hooks/product/useProductLists.ts +4 -6
- package/src/hooks/product/useProductMedia.ts +6 -17
- package/src/hooks/product/useProductSearch.ts +19 -15
- package/src/hooks/product/useProductVariants.ts +5 -13
- package/src/hooks/product/useProducts.ts +8 -12
- package/src/hooks/product/useRecommendedProducts.ts +4 -6
- package/src/hooks/shop/useRecommendedShops.ts +4 -6
- package/src/hooks/shop/useShop.ts +8 -12
- package/src/hooks/user/useBuyerAttributes.ts +4 -6
- package/src/hooks/user/useCurrentUser.ts +4 -6
- package/src/hooks/user/useFollowedShops.ts +5 -13
- package/src/hooks/user/useOrders.ts +4 -6
- package/src/hooks/user/useRecentProducts.ts +4 -6
- package/src/hooks/user/useRecentShops.ts +5 -13
- package/src/hooks/user/useSavedProducts.ts +5 -13
- package/src/hooks/util/useImagePicker.test.tsx +193 -0
- package/src/hooks/util/useImagePicker.ts +24 -5
- package/src/internal/reactQuery/MinisQueryProvider.test.tsx +38 -0
- package/src/internal/reactQuery/MinisQueryProvider.tsx +16 -0
- package/src/internal/reactQuery/index.ts +8 -0
- package/src/internal/reactQuery/queryClient.test.tsx +91 -0
- package/src/internal/reactQuery/queryClient.ts +43 -0
- package/src/internal/reactQuery/useShopActionInfiniteQuery.test.tsx +357 -0
- package/src/internal/reactQuery/useShopActionInfiniteQuery.ts +129 -0
- package/src/internal/reactQuery/useShopActionQuery.test.tsx +184 -0
- package/src/internal/reactQuery/useShopActionQuery.ts +74 -0
- package/src/internal/utils/resizeImage.test.ts +314 -0
- package/src/internal/utils/resizeImage.ts +108 -0
- package/src/providers/ImagePickerProvider.test.tsx +32 -1
- package/src/providers/ImagePickerProvider.tsx +108 -65
- package/dist/internal/useShopActionsDataFetching.js +0 -79
- package/dist/internal/useShopActionsDataFetching.js.map +0 -1
- package/dist/internal/useShopActionsPaginatedDataFetching.js +0 -96
- package/dist/internal/useShopActionsPaginatedDataFetching.js.map +0 -1
- package/src/hooks/product/useProductSearch.test.ts +0 -470
- package/src/internal/useShopActionsDataFetching.test.ts +0 -465
- package/src/internal/useShopActionsDataFetching.ts +0 -150
- package/src/internal/useShopActionsPaginatedDataFetching.ts +0 -188
- package/src/stories/Accordion.stories.tsx +0 -124
- package/src/stories/AddToCart.stories.tsx +0 -251
- package/src/stories/Alert.stories.tsx +0 -38
- package/src/stories/AlertDialog.stories.tsx +0 -48
- package/src/stories/Avatar.stories.tsx +0 -29
- package/src/stories/Badge.stories.tsx +0 -46
- package/src/stories/Button.stories.tsx +0 -81
- package/src/stories/Card.stories.tsx +0 -40
- package/src/stories/Checkbox.stories.tsx +0 -44
- package/src/stories/FavoriteButton.stories.tsx +0 -58
- package/src/stories/IconButton.stories.tsx +0 -68
- package/src/stories/ImageContentWrapper.stories.tsx +0 -65
- package/src/stories/Input.stories.tsx +0 -44
- package/src/stories/Label.stories.tsx +0 -19
- package/src/stories/List.stories.tsx +0 -64
- package/src/stories/MerchantCard.stories.tsx +0 -127
- package/src/stories/ProductCard.stories.tsx +0 -92
- package/src/stories/ProductLink.stories.tsx +0 -46
- package/src/stories/ProductVariantPrice.stories.tsx +0 -70
- package/src/stories/Progress.stories.tsx +0 -30
- package/src/stories/PullToRefreshList.stories.tsx +0 -122
- package/src/stories/QuantitySelector.stories.tsx +0 -78
- package/src/stories/RadioGroup.stories.tsx +0 -51
- package/src/stories/Search.stories.tsx +0 -37
- package/src/stories/Select.stories.tsx +0 -85
- package/src/stories/Skeleton.stories.tsx +0 -19
- package/src/stories/TextInput.stories.tsx +0 -26
- package/src/stories/Toaster.stories.tsx +0 -46
- package/src/stories/Touchable.stories.tsx +0 -40
- package/src/stories/VideoPlayer.stories.tsx +0 -129
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {useCallback} from 'react'
|
|
2
|
+
|
|
3
|
+
import {ShopActionResult} from '@shopify/shop-minis-platform/actions'
|
|
4
|
+
import {useQuery} from '@tanstack/react-query'
|
|
5
|
+
|
|
6
|
+
import {DataHookFetchPolicy} from '../../types'
|
|
7
|
+
|
|
8
|
+
import {useShopMinisQueryClient} from './queryClient'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper to use React Query with Shop Actions (non-paginated)
|
|
12
|
+
* Replaces useShopActionsDataFetching
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const { data, loading, error, refetch } = useShopActionQuery(
|
|
17
|
+
* ['product', id],
|
|
18
|
+
* getProduct,
|
|
19
|
+
* { id },
|
|
20
|
+
* { skip: false }
|
|
21
|
+
* )
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function useShopActionQuery<
|
|
25
|
+
TData,
|
|
26
|
+
TParams extends {fetchPolicy?: DataHookFetchPolicy},
|
|
27
|
+
>(
|
|
28
|
+
queryKey: unknown[],
|
|
29
|
+
action: (params: TParams) => Promise<ShopActionResult<{data: TData}>>,
|
|
30
|
+
params: TParams,
|
|
31
|
+
options?: {
|
|
32
|
+
skip?: boolean
|
|
33
|
+
}
|
|
34
|
+
) {
|
|
35
|
+
const {skip = false} = options ?? {}
|
|
36
|
+
|
|
37
|
+
// Always use our SDK's QueryClient for isolation
|
|
38
|
+
const queryClient = useShopMinisQueryClient()
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
data,
|
|
42
|
+
error,
|
|
43
|
+
isLoading,
|
|
44
|
+
refetch: reactQueryRefetch,
|
|
45
|
+
} = useQuery(
|
|
46
|
+
{
|
|
47
|
+
queryKey,
|
|
48
|
+
queryFn: async () => {
|
|
49
|
+
const result = await action(params)
|
|
50
|
+
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
throw result.error
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result.data.data
|
|
56
|
+
},
|
|
57
|
+
enabled: !skip,
|
|
58
|
+
// Caching disabled by default (handled by Apollo)
|
|
59
|
+
// fetchPolicy param is passed through to the action (Apollo layer)
|
|
60
|
+
},
|
|
61
|
+
queryClient
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const refetch = useCallback(async () => {
|
|
65
|
+
await reactQueryRefetch()
|
|
66
|
+
}, [reactQueryRefetch])
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
data: data ?? null,
|
|
70
|
+
loading: isLoading,
|
|
71
|
+
error: error as Error | null,
|
|
72
|
+
refetch,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {resizeImage} from './resizeImage'
|
|
4
|
+
|
|
5
|
+
describe('resizeImage', () => {
|
|
6
|
+
let mockCanvas: any
|
|
7
|
+
let mockContext: any
|
|
8
|
+
let mockImage: any
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Mock canvas context
|
|
12
|
+
mockContext = {
|
|
13
|
+
drawImage: vi.fn(),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Mock canvas
|
|
17
|
+
mockCanvas = {
|
|
18
|
+
width: 0,
|
|
19
|
+
height: 0,
|
|
20
|
+
getContext: vi.fn(() => mockContext),
|
|
21
|
+
toBlob: vi.fn(),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Mock document.createElement
|
|
25
|
+
const originalCreateElement = document.createElement
|
|
26
|
+
document.createElement = vi.fn((tag: string) => {
|
|
27
|
+
if (tag === 'canvas') {
|
|
28
|
+
return mockCanvas as any
|
|
29
|
+
}
|
|
30
|
+
return originalCreateElement(tag)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Mock URL methods
|
|
34
|
+
global.URL.createObjectURL = vi.fn(() => 'blob:mock-url')
|
|
35
|
+
global.URL.revokeObjectURL = vi.fn()
|
|
36
|
+
|
|
37
|
+
// Mock Image constructor
|
|
38
|
+
mockImage = {
|
|
39
|
+
width: 3000,
|
|
40
|
+
height: 4000,
|
|
41
|
+
onload: null as any,
|
|
42
|
+
onerror: null as any,
|
|
43
|
+
src: '',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
global.Image = vi.fn(() => mockImage) as any
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('Quality settings', () => {
|
|
50
|
+
it('returns original file for "original" quality', async () => {
|
|
51
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
52
|
+
|
|
53
|
+
const result = await resizeImage({
|
|
54
|
+
file: originalFile,
|
|
55
|
+
quality: 'original',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(result).toBe(originalFile)
|
|
59
|
+
expect(global.Image).not.toHaveBeenCalled()
|
|
60
|
+
expect(document.createElement).not.toHaveBeenCalled()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('resizes to 1080px for low quality', async () => {
|
|
64
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
65
|
+
|
|
66
|
+
// Set up canvas toBlob to call callback
|
|
67
|
+
mockCanvas.toBlob.mockImplementation((callback: any) => {
|
|
68
|
+
const blob = new Blob(['resized'], {type: 'image/jpeg'})
|
|
69
|
+
callback(blob)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Trigger image load after src is set
|
|
73
|
+
global.Image = vi.fn(() => {
|
|
74
|
+
const img = mockImage
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
if (img.onload) img.onload()
|
|
77
|
+
}, 0)
|
|
78
|
+
return img
|
|
79
|
+
}) as any
|
|
80
|
+
|
|
81
|
+
const resultPromise = resizeImage({
|
|
82
|
+
file: originalFile,
|
|
83
|
+
quality: 'low',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
87
|
+
|
|
88
|
+
const result = await resultPromise
|
|
89
|
+
|
|
90
|
+
expect(result).toBeInstanceOf(File)
|
|
91
|
+
expect(result.type).toBe('image/jpeg')
|
|
92
|
+
expect(result.name).toBe('test.jpg')
|
|
93
|
+
|
|
94
|
+
// Check canvas dimensions were set correctly (maintaining aspect ratio)
|
|
95
|
+
expect(mockCanvas.width).toBe(810) // 1080 * (3000/4000)
|
|
96
|
+
expect(mockCanvas.height).toBe(1080)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('resizes to 1600px for medium quality', async () => {
|
|
100
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
101
|
+
|
|
102
|
+
mockCanvas.toBlob.mockImplementation((callback: any) => {
|
|
103
|
+
const blob = new Blob(['resized'], {type: 'image/jpeg'})
|
|
104
|
+
callback(blob)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
global.Image = vi.fn(() => {
|
|
108
|
+
const img = mockImage
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
if (img.onload) img.onload()
|
|
111
|
+
}, 0)
|
|
112
|
+
return img
|
|
113
|
+
}) as any
|
|
114
|
+
|
|
115
|
+
const result = await resizeImage({
|
|
116
|
+
file: originalFile,
|
|
117
|
+
quality: 'medium',
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(result).toBeInstanceOf(File)
|
|
121
|
+
expect(mockCanvas.width).toBe(1200) // 1600 * (3000/4000)
|
|
122
|
+
expect(mockCanvas.height).toBe(1600)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('resizes to 2048px for high quality', async () => {
|
|
126
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
127
|
+
|
|
128
|
+
mockCanvas.toBlob.mockImplementation((callback: any) => {
|
|
129
|
+
const blob = new Blob(['resized'], {type: 'image/jpeg'})
|
|
130
|
+
callback(blob)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
global.Image = vi.fn(() => {
|
|
134
|
+
const img = mockImage
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
if (img.onload) img.onload()
|
|
137
|
+
}, 0)
|
|
138
|
+
return img
|
|
139
|
+
}) as any
|
|
140
|
+
|
|
141
|
+
const result = await resizeImage({
|
|
142
|
+
file: originalFile,
|
|
143
|
+
quality: 'high',
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(result).toBeInstanceOf(File)
|
|
147
|
+
expect(mockCanvas.width).toBe(1536) // 2048 * (3000/4000)
|
|
148
|
+
expect(mockCanvas.height).toBe(2048)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('Custom quality', () => {
|
|
153
|
+
it('uses custom size when provided', async () => {
|
|
154
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
155
|
+
|
|
156
|
+
mockCanvas.toBlob.mockImplementation((callback: any) => {
|
|
157
|
+
const blob = new Blob(['resized'], {type: 'image/jpeg'})
|
|
158
|
+
callback(blob)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
global.Image = vi.fn(() => {
|
|
162
|
+
const img = mockImage
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
if (img.onload) img.onload()
|
|
165
|
+
}, 0)
|
|
166
|
+
return img
|
|
167
|
+
}) as any
|
|
168
|
+
|
|
169
|
+
const result = await resizeImage({
|
|
170
|
+
file: originalFile,
|
|
171
|
+
quality: 'low',
|
|
172
|
+
customQuality: {size: 500, compression: 0.6},
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(result).toBeInstanceOf(File)
|
|
176
|
+
expect(mockCanvas.width).toBe(375) // 500 * (3000/4000)
|
|
177
|
+
expect(mockCanvas.height).toBe(500)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('Aspect ratio handling', () => {
|
|
182
|
+
it('maintains aspect ratio for landscape images', async () => {
|
|
183
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
184
|
+
|
|
185
|
+
// Set landscape dimensions
|
|
186
|
+
mockImage.width = 4000
|
|
187
|
+
mockImage.height = 3000
|
|
188
|
+
|
|
189
|
+
mockCanvas.toBlob.mockImplementation((callback: any) => {
|
|
190
|
+
const blob = new Blob(['resized'], {type: 'image/jpeg'})
|
|
191
|
+
callback(blob)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
global.Image = vi.fn(() => {
|
|
195
|
+
const img = mockImage
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
if (img.onload) img.onload()
|
|
198
|
+
}, 0)
|
|
199
|
+
return img
|
|
200
|
+
}) as any
|
|
201
|
+
|
|
202
|
+
await resizeImage({
|
|
203
|
+
file: originalFile,
|
|
204
|
+
quality: 'low',
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
expect(mockCanvas.width).toBe(1080)
|
|
208
|
+
expect(mockCanvas.height).toBe(810) // 1080 * (3000/4000)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('does not resize if image is smaller than target size', async () => {
|
|
212
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
213
|
+
|
|
214
|
+
// Set small dimensions
|
|
215
|
+
mockImage.width = 800
|
|
216
|
+
mockImage.height = 600
|
|
217
|
+
|
|
218
|
+
mockCanvas.toBlob.mockImplementation((callback: any) => {
|
|
219
|
+
const blob = new Blob(['resized'], {type: 'image/jpeg'})
|
|
220
|
+
callback(blob)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
global.Image = vi.fn(() => {
|
|
224
|
+
const img = mockImage
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
if (img.onload) img.onload()
|
|
227
|
+
}, 0)
|
|
228
|
+
return img
|
|
229
|
+
}) as any
|
|
230
|
+
|
|
231
|
+
await resizeImage({
|
|
232
|
+
file: originalFile,
|
|
233
|
+
quality: 'high',
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// Should maintain original dimensions
|
|
237
|
+
expect(mockCanvas.width).toBe(800)
|
|
238
|
+
expect(mockCanvas.height).toBe(600)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('Error handling', () => {
|
|
243
|
+
it('rejects when image fails to load', async () => {
|
|
244
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
245
|
+
|
|
246
|
+
global.Image = vi.fn(() => {
|
|
247
|
+
const img = mockImage
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
if (img.onerror) img.onerror()
|
|
250
|
+
}, 0)
|
|
251
|
+
return img
|
|
252
|
+
}) as any
|
|
253
|
+
|
|
254
|
+
await expect(
|
|
255
|
+
resizeImage({
|
|
256
|
+
file: originalFile,
|
|
257
|
+
quality: 'medium',
|
|
258
|
+
})
|
|
259
|
+
).rejects.toThrow('Failed to load image')
|
|
260
|
+
|
|
261
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('Resource cleanup', () => {
|
|
266
|
+
it('revokes object URL after successful resize', async () => {
|
|
267
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
268
|
+
|
|
269
|
+
mockCanvas.toBlob.mockImplementation((callback: any) => {
|
|
270
|
+
const blob = new Blob(['resized'], {type: 'image/jpeg'})
|
|
271
|
+
callback(blob)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
global.Image = vi.fn(() => {
|
|
275
|
+
const img = mockImage
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
if (img.onload) img.onload()
|
|
278
|
+
}, 0)
|
|
279
|
+
return img
|
|
280
|
+
}) as any
|
|
281
|
+
|
|
282
|
+
await resizeImage({
|
|
283
|
+
file: originalFile,
|
|
284
|
+
quality: 'medium',
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
expect(URL.createObjectURL).toHaveBeenCalledWith(originalFile)
|
|
288
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('revokes object URL even when error occurs', async () => {
|
|
292
|
+
const originalFile = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
293
|
+
|
|
294
|
+
global.Image = vi.fn(() => {
|
|
295
|
+
const img = mockImage
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
if (img.onerror) img.onerror()
|
|
298
|
+
}, 0)
|
|
299
|
+
return img
|
|
300
|
+
}) as any
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await resizeImage({
|
|
304
|
+
file: originalFile,
|
|
305
|
+
quality: 'medium',
|
|
306
|
+
})
|
|
307
|
+
} catch {
|
|
308
|
+
// Expected to throw
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ImageQuality,
|
|
3
|
+
CustomImageQuality,
|
|
4
|
+
} from '../../providers/ImagePickerProvider'
|
|
5
|
+
|
|
6
|
+
interface ResizeSettings {
|
|
7
|
+
size: number
|
|
8
|
+
compression: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResizeImageParams {
|
|
12
|
+
file: File
|
|
13
|
+
quality: ImageQuality
|
|
14
|
+
customQuality?: CustomImageQuality
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const QUALITY_SETTINGS: {
|
|
18
|
+
[key in Exclude<ImageQuality, 'original'>]: ResizeSettings
|
|
19
|
+
} = {
|
|
20
|
+
low: {
|
|
21
|
+
size: 1080,
|
|
22
|
+
compression: 0.7,
|
|
23
|
+
},
|
|
24
|
+
medium: {
|
|
25
|
+
size: 1600,
|
|
26
|
+
compression: 0.85,
|
|
27
|
+
},
|
|
28
|
+
high: {
|
|
29
|
+
size: 2048,
|
|
30
|
+
compression: 0.92,
|
|
31
|
+
},
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
export function resizeImage({
|
|
35
|
+
file,
|
|
36
|
+
quality,
|
|
37
|
+
customQuality,
|
|
38
|
+
}: ResizeImageParams): Promise<File> {
|
|
39
|
+
if (quality === 'original') {
|
|
40
|
+
return Promise.resolve(file)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const defaultSettings = QUALITY_SETTINGS[quality]
|
|
44
|
+
const settings: ResizeSettings = customQuality
|
|
45
|
+
? {
|
|
46
|
+
size: customQuality.size ?? defaultSettings.size,
|
|
47
|
+
compression: customQuality.compression ?? defaultSettings.compression,
|
|
48
|
+
}
|
|
49
|
+
: defaultSettings
|
|
50
|
+
const maxSize = settings.size
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const img = new Image()
|
|
54
|
+
const url = URL.createObjectURL(file)
|
|
55
|
+
|
|
56
|
+
img.onerror = () => {
|
|
57
|
+
URL.revokeObjectURL(url)
|
|
58
|
+
reject(new Error('Failed to load image'))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
img.onload = () => {
|
|
62
|
+
URL.revokeObjectURL(url)
|
|
63
|
+
|
|
64
|
+
const canvas = document.createElement('canvas')
|
|
65
|
+
let width = img.width
|
|
66
|
+
let height = img.height
|
|
67
|
+
|
|
68
|
+
// Resize image dimensions maintaining aspect ratio
|
|
69
|
+
if (width > height) {
|
|
70
|
+
if (width > maxSize) {
|
|
71
|
+
height *= maxSize / width
|
|
72
|
+
width = maxSize
|
|
73
|
+
}
|
|
74
|
+
} else if (height > maxSize) {
|
|
75
|
+
width *= maxSize / height
|
|
76
|
+
height = maxSize
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
canvas.width = width
|
|
80
|
+
canvas.height = height
|
|
81
|
+
|
|
82
|
+
const ctx = canvas.getContext('2d')
|
|
83
|
+
if (!ctx) {
|
|
84
|
+
reject(new Error('Failed to get canvas context'))
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
ctx.drawImage(img, 0, 0, width, height)
|
|
88
|
+
|
|
89
|
+
canvas.toBlob(
|
|
90
|
+
blob => {
|
|
91
|
+
if (!blob) {
|
|
92
|
+
reject(new Error('Failed to create blob'))
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const resizedFile = new File([blob], file.name, {
|
|
96
|
+
type: 'image/jpeg',
|
|
97
|
+
lastModified: Date.now(),
|
|
98
|
+
})
|
|
99
|
+
resolve(resizedFile)
|
|
100
|
+
},
|
|
101
|
+
'image/jpeg',
|
|
102
|
+
settings.compression
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
img.src = url
|
|
107
|
+
})
|
|
108
|
+
}
|
|
@@ -34,6 +34,37 @@ describe('ImagePickerProvider', () => {
|
|
|
34
34
|
mockRequestPermission.mockResolvedValue({granted: true})
|
|
35
35
|
// Clear interaction reporting mock
|
|
36
36
|
mockReportInteraction.mockClear()
|
|
37
|
+
|
|
38
|
+
// Mock URL.createObjectURL and URL.revokeObjectURL for jsdom
|
|
39
|
+
global.URL.createObjectURL = vi.fn(() => 'blob:mock-url')
|
|
40
|
+
global.URL.revokeObjectURL = vi.fn()
|
|
41
|
+
|
|
42
|
+
// Mock Image constructor for resizing
|
|
43
|
+
global.Image = class MockImage {
|
|
44
|
+
width = 1920
|
|
45
|
+
height = 1080
|
|
46
|
+
onload = null as any
|
|
47
|
+
onerror = null as any
|
|
48
|
+
src = ''
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
if (this.onload) {
|
|
53
|
+
this.onload()
|
|
54
|
+
}
|
|
55
|
+
}, 0)
|
|
56
|
+
}
|
|
57
|
+
} as any
|
|
58
|
+
|
|
59
|
+
// Mock canvas methods
|
|
60
|
+
HTMLCanvasElement.prototype.getContext = vi.fn(() => ({
|
|
61
|
+
drawImage: vi.fn(),
|
|
62
|
+
})) as any
|
|
63
|
+
|
|
64
|
+
HTMLCanvasElement.prototype.toBlob = vi.fn(callback => {
|
|
65
|
+
const blob = new Blob(['mock'], {type: 'image/jpeg'})
|
|
66
|
+
callback(blob)
|
|
67
|
+
}) as any
|
|
37
68
|
})
|
|
38
69
|
|
|
39
70
|
afterEach(() => {
|
|
@@ -334,7 +365,7 @@ describe('ImagePickerProvider', () => {
|
|
|
334
365
|
<button
|
|
335
366
|
type="button"
|
|
336
367
|
onClick={() =>
|
|
337
|
-
openCamera('front').catch(() => {
|
|
368
|
+
openCamera({cameraFacing: 'front'}).catch(() => {
|
|
338
369
|
// Ignore errors from cleanup
|
|
339
370
|
})
|
|
340
371
|
}
|