@shopify/shop-minis-react 0.0.33 → 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/index10.js +2 -2
- package/dist/_virtual/index2.js +4 -4
- package/dist/_virtual/index3.js +4 -4
- package/dist/_virtual/index8.js +2 -2
- package/dist/_virtual/index9.js +2 -2
- 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/image.js +52 -0
- package/dist/components/atoms/image.js.map +1 -0
- 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 +2 -1
- package/dist/components/commerce/merchant-card.js.map +1 -1
- package/dist/components/commerce/product-card.js +11 -11
- package/dist/components/commerce/product-card.js.map +1 -1
- package/dist/components/content/image-content-wrapper.js +29 -22
- package/dist/components/content/image-content-wrapper.js.map +1 -1
- package/dist/components/ui/input.js +15 -9
- package/dist/components/ui/input.js.map +1 -1
- package/dist/hooks/content/useCreateImageContent.js +16 -22
- package/dist/hooks/content/useCreateImageContent.js.map +1 -1
- package/dist/hooks/storage/useImageUpload.js +36 -37
- package/dist/hooks/storage/useImageUpload.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 +218 -212
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +4 -1
- package/dist/mocks.js.map +1 -1
- package/dist/shop-minis-platform/src/types/content.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-string@1.9.1/node_modules/color-string/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/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/dist/utils/colors.js +1 -1
- package/dist/utils/image.js +46 -9
- 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/{thumbhash-image.tsx → image.tsx} +14 -14
- 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 +4 -2
- package/src/components/commerce/product-card.test.tsx +364 -0
- package/src/components/commerce/product-card.tsx +2 -2
- 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/content/image-content-wrapper.tsx +9 -2
- package/src/components/index.ts +2 -1
- 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/content/useCreateImageContent.ts +1 -7
- 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/storage/useImageUpload.ts +22 -20
- 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 +73 -0
- package/src/utils/index.ts +1 -1
- package/dist/components/atoms/thumbhash-image.js +0 -54
- package/dist/components/atoms/thumbhash-image.js.map +0 -1
- package/dist/utils/imageToDataUri.js +0 -10
- package/dist/utils/imageToDataUri.js.map +0 -1
- package/src/utils/imageToDataUri.ts +0 -8
|
@@ -1,22 +1,19 @@
|
|
|
1
|
+
/* eslint-disable jsx-a11y/alt-text */
|
|
1
2
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
|
2
3
|
import {ImgHTMLAttributes, useCallback, useMemo, memo, useState} from 'react'
|
|
3
4
|
|
|
4
5
|
import {cn} from '../../lib/utils'
|
|
5
|
-
import {getThumbhashDataURL} from '../../utils
|
|
6
|
+
import {getThumbhashDataURL, getResizedImageUrl} from '../../utils'
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
-
src
|
|
9
|
-
thumbhash
|
|
10
|
-
alt?: string | null
|
|
8
|
+
type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
|
|
9
|
+
src?: string
|
|
10
|
+
thumbhash?: string | null
|
|
11
11
|
aspectRatio?: number | string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export const
|
|
15
|
-
props: ThumbhashImageProps
|
|
16
|
-
) {
|
|
14
|
+
export const Image = memo(function Image(props: ImageProps) {
|
|
17
15
|
const {
|
|
18
16
|
src,
|
|
19
|
-
alt,
|
|
20
17
|
thumbhash,
|
|
21
18
|
onLoad,
|
|
22
19
|
className,
|
|
@@ -27,7 +24,7 @@ export const ThumbhashImage = memo(function ThumbhashImage(
|
|
|
27
24
|
|
|
28
25
|
const [isLoaded, setIsLoaded] = useState(false)
|
|
29
26
|
|
|
30
|
-
const
|
|
27
|
+
const thumbhashDataURL = useMemo(
|
|
31
28
|
() => getThumbhashDataURL(thumbhash ?? undefined),
|
|
32
29
|
[thumbhash]
|
|
33
30
|
)
|
|
@@ -40,24 +37,27 @@ export const ThumbhashImage = memo(function ThumbhashImage(
|
|
|
40
37
|
[onLoad]
|
|
41
38
|
)
|
|
42
39
|
|
|
40
|
+
const resizedImageSrc = useMemo(() => getResizedImageUrl(src), [src])
|
|
41
|
+
|
|
43
42
|
return (
|
|
44
43
|
<div
|
|
45
44
|
className={cn('relative w-full ', className)}
|
|
46
45
|
style={{
|
|
47
46
|
...style,
|
|
48
47
|
aspectRatio,
|
|
49
|
-
backgroundImage:
|
|
48
|
+
backgroundImage: thumbhashDataURL
|
|
49
|
+
? `url(${thumbhashDataURL})`
|
|
50
|
+
: undefined,
|
|
50
51
|
backgroundSize: 'cover',
|
|
51
52
|
backgroundPosition: 'center',
|
|
52
53
|
}}
|
|
53
54
|
>
|
|
54
55
|
<img
|
|
55
56
|
className={cn(
|
|
56
|
-
'absolute inset-0
|
|
57
|
+
'absolute inset-0 opacity-0 object-cover',
|
|
57
58
|
isLoaded && 'opacity-100'
|
|
58
59
|
)}
|
|
59
|
-
src={
|
|
60
|
-
alt={alt}
|
|
60
|
+
src={resizedImageSrc}
|
|
61
61
|
onLoad={handleLoad}
|
|
62
62
|
{...restProps}
|
|
63
63
|
/>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {render, screen} from '../../test-utils'
|
|
4
|
+
|
|
5
|
+
import {ProductVariantPrice} from './product-variant-price'
|
|
6
|
+
|
|
7
|
+
// Mock formatMoney function
|
|
8
|
+
vi.mock('../../lib/formatMoney', () => ({
|
|
9
|
+
formatMoney: vi.fn((amount: string, currencyCode: string) => {
|
|
10
|
+
const numAmount = parseFloat(amount)
|
|
11
|
+
return currencyCode === 'USD'
|
|
12
|
+
? `$${numAmount.toFixed(2)}`
|
|
13
|
+
: `${currencyCode} ${numAmount.toFixed(2)}`
|
|
14
|
+
}),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
describe('ProductVariantPrice', () => {
|
|
18
|
+
it('renders nothing when amount is missing', () => {
|
|
19
|
+
const {container} = render(
|
|
20
|
+
<ProductVariantPrice amount="" currencyCode="USD" />
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
expect(container.firstChild).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('renders nothing when currencyCode is missing', () => {
|
|
27
|
+
const {container} = render(
|
|
28
|
+
<ProductVariantPrice amount="19.99" currencyCode="" />
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
expect(container.firstChild).toBeNull()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders single price when no compare at price', () => {
|
|
35
|
+
render(<ProductVariantPrice amount="19.99" currencyCode="USD" />)
|
|
36
|
+
|
|
37
|
+
const price = screen.getByText('$19.99')
|
|
38
|
+
expect(price).not.toHaveClass('line-through')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('renders both prices when compare at price is provided and different', () => {
|
|
42
|
+
render(
|
|
43
|
+
<ProductVariantPrice
|
|
44
|
+
amount="19.99"
|
|
45
|
+
currencyCode="USD"
|
|
46
|
+
compareAtPriceAmount="29.99"
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText('$19.99')).toBeInTheDocument()
|
|
51
|
+
expect(screen.getByText('$29.99')).toBeInTheDocument()
|
|
52
|
+
|
|
53
|
+
const compareAtPrice = screen.getByText('$29.99')
|
|
54
|
+
expect(compareAtPrice).toHaveClass('line-through')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders single price when compare at price equals current price', () => {
|
|
58
|
+
render(
|
|
59
|
+
<ProductVariantPrice
|
|
60
|
+
amount="19.99"
|
|
61
|
+
currencyCode="USD"
|
|
62
|
+
compareAtPriceAmount="19.99"
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const prices = screen.getAllByText('$19.99')
|
|
67
|
+
expect(prices).toHaveLength(1)
|
|
68
|
+
expect(prices[0]).not.toHaveClass('line-through')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('handles different currency codes', () => {
|
|
72
|
+
render(
|
|
73
|
+
<ProductVariantPrice
|
|
74
|
+
amount="19.99"
|
|
75
|
+
currencyCode="EUR"
|
|
76
|
+
compareAtPriceAmount="29.99"
|
|
77
|
+
compareAtPriceCurrencyCode="GBP"
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(screen.getByText('EUR 19.99')).toBeInTheDocument()
|
|
82
|
+
expect(screen.getByText('GBP 29.99')).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('applies custom className', () => {
|
|
86
|
+
render(
|
|
87
|
+
<ProductVariantPrice
|
|
88
|
+
amount="19.99"
|
|
89
|
+
currencyCode="USD"
|
|
90
|
+
className="custom-class"
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const container = screen.getByText('$19.99').parentElement
|
|
95
|
+
expect(container).toHaveClass('custom-class')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('applies custom price classNames', () => {
|
|
99
|
+
render(
|
|
100
|
+
<ProductVariantPrice
|
|
101
|
+
amount="19.99"
|
|
102
|
+
currencyCode="USD"
|
|
103
|
+
compareAtPriceAmount="29.99"
|
|
104
|
+
currentPriceClassName="current-custom"
|
|
105
|
+
originalPriceClassName="original-custom"
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const currentPrice = screen.getByText('$19.99')
|
|
110
|
+
expect(currentPrice).toHaveClass('current-custom')
|
|
111
|
+
|
|
112
|
+
const originalPrice = screen.getByText('$29.99')
|
|
113
|
+
expect(originalPrice).toHaveClass('original-custom')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('handles string and number amounts', () => {
|
|
117
|
+
render(
|
|
118
|
+
<ProductVariantPrice
|
|
119
|
+
amount={19.99}
|
|
120
|
+
currencyCode="USD"
|
|
121
|
+
compareAtPriceAmount={29.99}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
expect(screen.getByText('$19.99')).toBeInTheDocument()
|
|
126
|
+
expect(screen.getByText('$29.99')).toBeInTheDocument()
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
4
|
+
import {render, screen, userEvent} from '../../test-utils'
|
|
5
|
+
|
|
6
|
+
import {TextInput} from './text-input'
|
|
7
|
+
|
|
8
|
+
vi.mock('../../internal/useShopActions', () => ({
|
|
9
|
+
useShopActions: vi.fn(() => ({
|
|
10
|
+
translateContentUp: vi.fn(),
|
|
11
|
+
translateContentDown: vi.fn(),
|
|
12
|
+
})),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
describe('TextInput', () => {
|
|
16
|
+
let mockTranslateContentUp: ReturnType<typeof vi.fn>
|
|
17
|
+
let mockTranslateContentDown: ReturnType<typeof vi.fn>
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks()
|
|
21
|
+
|
|
22
|
+
mockTranslateContentUp = vi.fn()
|
|
23
|
+
mockTranslateContentDown = vi.fn()
|
|
24
|
+
;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
25
|
+
translateContentUp: mockTranslateContentUp,
|
|
26
|
+
translateContentDown: mockTranslateContentDown,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('forwards all input props correctly', () => {
|
|
31
|
+
render(
|
|
32
|
+
<TextInput
|
|
33
|
+
placeholder="Test input"
|
|
34
|
+
value="test value"
|
|
35
|
+
disabled
|
|
36
|
+
type="email"
|
|
37
|
+
className="custom-class"
|
|
38
|
+
id="test-input"
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const input = screen.getByPlaceholderText('Test input')
|
|
43
|
+
expect(input).toHaveValue('test value')
|
|
44
|
+
expect(input).toBeDisabled()
|
|
45
|
+
expect(input).toHaveAttribute('type', 'email')
|
|
46
|
+
expect(input).toHaveClass('custom-class')
|
|
47
|
+
expect(input).toHaveAttribute('id', 'test-input')
|
|
48
|
+
|
|
49
|
+
expect(input).toBeInstanceOf(HTMLInputElement)
|
|
50
|
+
expect(input.tagName).toBe('INPUT')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('calls translateContentUp on focus', async () => {
|
|
54
|
+
const user = userEvent.setup()
|
|
55
|
+
|
|
56
|
+
render(<TextInput placeholder="Test input" />)
|
|
57
|
+
|
|
58
|
+
const input = screen.getByPlaceholderText('Test input')
|
|
59
|
+
|
|
60
|
+
await user.click(input)
|
|
61
|
+
|
|
62
|
+
expect(mockTranslateContentUp).toHaveBeenCalledTimes(1)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('calls translateContentDown on blur', async () => {
|
|
66
|
+
const user = userEvent.setup()
|
|
67
|
+
|
|
68
|
+
render(<TextInput placeholder="Test input" />)
|
|
69
|
+
|
|
70
|
+
const input = screen.getByPlaceholderText('Test input')
|
|
71
|
+
|
|
72
|
+
await user.click(input)
|
|
73
|
+
await user.tab()
|
|
74
|
+
|
|
75
|
+
expect(mockTranslateContentDown).toHaveBeenCalledTimes(1)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('calls custom onFocus handler when provided', async () => {
|
|
79
|
+
const user = userEvent.setup()
|
|
80
|
+
const customOnFocus = vi.fn()
|
|
81
|
+
|
|
82
|
+
render(<TextInput placeholder="Test input" onFocus={customOnFocus} />)
|
|
83
|
+
|
|
84
|
+
const input = screen.getByPlaceholderText('Test input')
|
|
85
|
+
await user.click(input)
|
|
86
|
+
|
|
87
|
+
expect(customOnFocus).toHaveBeenCalledTimes(1)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('calls custom onBlur handler when provided', async () => {
|
|
91
|
+
const user = userEvent.setup()
|
|
92
|
+
const customOnBlur = vi.fn()
|
|
93
|
+
|
|
94
|
+
render(<TextInput placeholder="Test input" onBlur={customOnBlur} />)
|
|
95
|
+
|
|
96
|
+
const input = screen.getByPlaceholderText('Test input')
|
|
97
|
+
|
|
98
|
+
// Focus first, then blur
|
|
99
|
+
await user.click(input)
|
|
100
|
+
await user.tab()
|
|
101
|
+
|
|
102
|
+
expect(customOnBlur).toHaveBeenCalledTimes(1)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import {useKeyboardAvoidingView} from '../../hooks'
|
|
4
|
+
import {Input} from '../ui/input'
|
|
5
|
+
|
|
6
|
+
function TextInput({...props}: React.ComponentProps<'input'>) {
|
|
7
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
8
|
+
const {onBlur, onFocus} = useKeyboardAvoidingView()
|
|
9
|
+
|
|
10
|
+
const _onFocus = React.useCallback(
|
|
11
|
+
(event: React.FocusEvent<HTMLInputElement>) => {
|
|
12
|
+
onFocus(inputRef)
|
|
13
|
+
props.onFocus?.(event)
|
|
14
|
+
},
|
|
15
|
+
[props, onFocus, inputRef]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const _onBlur = React.useCallback(
|
|
19
|
+
(event: React.FocusEvent<HTMLInputElement>) => {
|
|
20
|
+
onBlur()
|
|
21
|
+
props.onBlur?.(event)
|
|
22
|
+
},
|
|
23
|
+
[props, onBlur]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Input innerRef={inputRef} onFocus={_onFocus} onBlur={_onBlur} {...props} />
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {TextInput}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
userEvent,
|
|
7
|
+
mockShop,
|
|
8
|
+
mockMinisSDK,
|
|
9
|
+
resetAllMocks,
|
|
10
|
+
} from '../../test-utils'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
MerchantCard,
|
|
14
|
+
MerchantCardContainer,
|
|
15
|
+
MerchantCardHeader,
|
|
16
|
+
MerchantCardInfo,
|
|
17
|
+
MerchantCardName,
|
|
18
|
+
MerchantCardRating,
|
|
19
|
+
} from './merchant-card'
|
|
20
|
+
|
|
21
|
+
// Mock hooks
|
|
22
|
+
vi.mock('../../hooks/navigation/useShopNavigation', () => ({
|
|
23
|
+
useShopNavigation: () => ({
|
|
24
|
+
navigateToShop: mockMinisSDK.navigateToShop,
|
|
25
|
+
navigateToProduct: mockMinisSDK.navigateToProduct,
|
|
26
|
+
}),
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
// Mock utils with simple implementations
|
|
30
|
+
vi.mock('../../utils', () => ({
|
|
31
|
+
extractBrandTheme: vi.fn(() => ({
|
|
32
|
+
type: 'default',
|
|
33
|
+
backgroundColor: 'white',
|
|
34
|
+
})),
|
|
35
|
+
getFeaturedImages: vi.fn(() => []),
|
|
36
|
+
formatReviewCount: vi.fn((count: number) => {
|
|
37
|
+
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`
|
|
38
|
+
return count.toString()
|
|
39
|
+
}),
|
|
40
|
+
normalizeRating: vi.fn((rating: number) => rating.toFixed(1)),
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
vi.mock('../../utils/colors', () => ({
|
|
44
|
+
isDarkColor: vi.fn(() => false),
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
describe('MerchantCard', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
resetAllMocks()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.clearAllMocks()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('Rendering', () => {
|
|
57
|
+
it('renders with basic shop information', () => {
|
|
58
|
+
const shop = mockShop()
|
|
59
|
+
render(<MerchantCard shop={shop} />)
|
|
60
|
+
|
|
61
|
+
expect(screen.getByText(shop.name)).toBeInTheDocument()
|
|
62
|
+
// createShop returns 4.3 rating with 50 reviews
|
|
63
|
+
expect(screen.getByText('4.3 (50)')).toBeInTheDocument() // Rating and review count
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('renders shop logo when available', () => {
|
|
67
|
+
const shop = mockShop({
|
|
68
|
+
visualTheme: {
|
|
69
|
+
logoImage: {
|
|
70
|
+
url: 'https://example.com/logo.png',
|
|
71
|
+
altText: 'Shop Logo',
|
|
72
|
+
sensitive: false,
|
|
73
|
+
},
|
|
74
|
+
featuredImages: [],
|
|
75
|
+
id: '123',
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
render(<MerchantCard shop={shop} />)
|
|
80
|
+
|
|
81
|
+
const logo = screen.getByAltText(`${shop.name} logo`)
|
|
82
|
+
expect(logo).toBeInTheDocument()
|
|
83
|
+
expect(logo).toHaveAttribute('src', 'https://example.com/logo.png')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('renders shop initial when logo is not available', () => {
|
|
87
|
+
const shop = mockShop({
|
|
88
|
+
name: 'Test Shop',
|
|
89
|
+
visualTheme: null,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
render(<MerchantCard shop={shop} />)
|
|
93
|
+
|
|
94
|
+
expect(screen.getByText('T')).toBeInTheDocument()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('renders without reviews when not available', () => {
|
|
98
|
+
const shop = mockShop({
|
|
99
|
+
reviewAnalytics: {
|
|
100
|
+
averageRating: null,
|
|
101
|
+
reviewCount: 0,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
render(<MerchantCard shop={shop} />)
|
|
106
|
+
|
|
107
|
+
expect(
|
|
108
|
+
screen.queryByTestId('merchant-card-rating')
|
|
109
|
+
).not.toBeInTheDocument()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('Interactions', () => {
|
|
114
|
+
it('handles shop click navigation', async () => {
|
|
115
|
+
const user = userEvent.setup()
|
|
116
|
+
const shop = mockShop()
|
|
117
|
+
|
|
118
|
+
render(<MerchantCard shop={shop} />)
|
|
119
|
+
|
|
120
|
+
const card = screen.getByText(shop.name).closest('div')
|
|
121
|
+
?.parentElement?.parentElement
|
|
122
|
+
await user.click(card!)
|
|
123
|
+
|
|
124
|
+
expect(mockMinisSDK.navigateToShop).toHaveBeenCalledWith({
|
|
125
|
+
shopId: shop.id,
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('does not navigate when touchable is false', async () => {
|
|
130
|
+
const user = userEvent.setup()
|
|
131
|
+
const shop = mockShop()
|
|
132
|
+
|
|
133
|
+
render(<MerchantCard shop={shop} touchable={false} />)
|
|
134
|
+
|
|
135
|
+
const card = screen.getByText(shop.name).closest('div')
|
|
136
|
+
?.parentElement?.parentElement
|
|
137
|
+
await user.click(card!)
|
|
138
|
+
|
|
139
|
+
expect(mockMinisSDK.navigateToShop).not.toHaveBeenCalled()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('Custom Composition', () => {
|
|
144
|
+
it('renders with custom children layout', () => {
|
|
145
|
+
const shop = mockShop()
|
|
146
|
+
|
|
147
|
+
render(
|
|
148
|
+
<MerchantCard shop={shop}>
|
|
149
|
+
<MerchantCardContainer>
|
|
150
|
+
<div data-testid="custom-layout">
|
|
151
|
+
<MerchantCardName>Custom Name</MerchantCardName>
|
|
152
|
+
<MerchantCardRating />
|
|
153
|
+
</div>
|
|
154
|
+
</MerchantCardContainer>
|
|
155
|
+
</MerchantCard>
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
expect(screen.getByTestId('custom-layout')).toBeInTheDocument()
|
|
159
|
+
expect(screen.getByText('Custom Name')).toBeInTheDocument()
|
|
160
|
+
// createShop returns 4.3 rating with 50 reviews
|
|
161
|
+
expect(screen.getByText('4.3 (50)')).toBeInTheDocument()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('allows individual component usage', () => {
|
|
165
|
+
const shop = mockShop()
|
|
166
|
+
|
|
167
|
+
render(
|
|
168
|
+
<MerchantCard shop={shop}>
|
|
169
|
+
<MerchantCardContainer>
|
|
170
|
+
<MerchantCardHeader withLogo />
|
|
171
|
+
<MerchantCardInfo>
|
|
172
|
+
<MerchantCardName />
|
|
173
|
+
<MerchantCardRating />
|
|
174
|
+
</MerchantCardInfo>
|
|
175
|
+
</MerchantCardContainer>
|
|
176
|
+
</MerchantCard>
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
expect(screen.getByText(shop.name)).toBeInTheDocument()
|
|
180
|
+
// createShop returns 4.3 rating with 50 reviews
|
|
181
|
+
expect(screen.getByText('4.3 (50)')).toBeInTheDocument()
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('Edge Cases', () => {
|
|
186
|
+
it('handles shop without visual theme', () => {
|
|
187
|
+
const shop = mockShop({
|
|
188
|
+
visualTheme: null,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
render(<MerchantCard shop={shop} />)
|
|
192
|
+
|
|
193
|
+
expect(screen.getByText(shop.name)).toBeInTheDocument()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('handles shop with empty name', () => {
|
|
197
|
+
const shop = mockShop({
|
|
198
|
+
name: '',
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
render(<MerchantCard shop={shop} />)
|
|
202
|
+
|
|
203
|
+
// Should still render with empty name
|
|
204
|
+
const heading = screen.getByRole('heading', {level: 3})
|
|
205
|
+
expect(heading).toBeInTheDocument()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('handles very long shop names', () => {
|
|
209
|
+
const shop = mockShop({
|
|
210
|
+
name: 'This is a very long shop name that should be truncated in the UI',
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
render(<MerchantCard shop={shop} />)
|
|
214
|
+
|
|
215
|
+
const nameElement = screen.getByText(shop.name)
|
|
216
|
+
expect(nameElement).toHaveClass('truncate')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('handles high review counts', () => {
|
|
220
|
+
const shop = mockShop({
|
|
221
|
+
reviewAnalytics: {
|
|
222
|
+
averageRating: 4.8,
|
|
223
|
+
reviewCount: 12500,
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
render(<MerchantCard shop={shop} />)
|
|
228
|
+
|
|
229
|
+
expect(screen.getByText('4.8 (12.5k)')).toBeInTheDocument()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('Accessibility', () => {
|
|
234
|
+
it('maintains proper heading hierarchy', () => {
|
|
235
|
+
const shop = mockShop()
|
|
236
|
+
render(<MerchantCard shop={shop} />)
|
|
237
|
+
|
|
238
|
+
const heading = screen.getByRole('heading', {level: 3})
|
|
239
|
+
expect(heading).toHaveTextContent(shop.name)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('provides fallback alt text for logo', () => {
|
|
243
|
+
const shop = mockShop({
|
|
244
|
+
visualTheme: {
|
|
245
|
+
logoImage: {
|
|
246
|
+
url: 'https://example.com/logo.png',
|
|
247
|
+
altText: null,
|
|
248
|
+
sensitive: false,
|
|
249
|
+
},
|
|
250
|
+
featuredImages: [],
|
|
251
|
+
id: '123',
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
render(<MerchantCard shop={shop} />)
|
|
256
|
+
|
|
257
|
+
const logo = screen.getByAltText(`${shop.name} logo`)
|
|
258
|
+
expect(logo).toBeInTheDocument()
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
normalizeRating,
|
|
15
15
|
} from '../../utils'
|
|
16
16
|
import {isDarkColor} from '../../utils/colors'
|
|
17
|
-
import {
|
|
17
|
+
import {Image} from '../atoms/image'
|
|
18
18
|
import {Touchable} from '../atoms/touchable'
|
|
19
19
|
|
|
20
20
|
interface MerchantCardContextValue {
|
|
@@ -100,7 +100,7 @@ function MerchantCardImage({
|
|
|
100
100
|
|
|
101
101
|
if (thumbhash) {
|
|
102
102
|
return (
|
|
103
|
-
<
|
|
103
|
+
<Image
|
|
104
104
|
data-slot="merchant-card-image"
|
|
105
105
|
src={src}
|
|
106
106
|
alt={alt}
|
|
@@ -274,6 +274,8 @@ function MerchantCardDefaultHeader({withLogo = false}: {withLogo?: boolean}) {
|
|
|
274
274
|
/>
|
|
275
275
|
)
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
return null
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
return (
|