@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.
Files changed (86) hide show
  1. package/dist/_virtual/index10.js +2 -2
  2. package/dist/_virtual/index2.js +4 -4
  3. package/dist/_virtual/index3.js +4 -4
  4. package/dist/_virtual/index8.js +2 -2
  5. package/dist/_virtual/index9.js +2 -2
  6. package/dist/components/atoms/alert-dialog.js.map +1 -1
  7. package/dist/components/atoms/icon-button.js +12 -12
  8. package/dist/components/atoms/icon-button.js.map +1 -1
  9. package/dist/components/atoms/image.js +52 -0
  10. package/dist/components/atoms/image.js.map +1 -0
  11. package/dist/components/atoms/text-input.js +22 -0
  12. package/dist/components/atoms/text-input.js.map +1 -0
  13. package/dist/components/commerce/merchant-card.js +2 -1
  14. package/dist/components/commerce/merchant-card.js.map +1 -1
  15. package/dist/components/commerce/product-card.js +11 -11
  16. package/dist/components/commerce/product-card.js.map +1 -1
  17. package/dist/components/content/image-content-wrapper.js +29 -22
  18. package/dist/components/content/image-content-wrapper.js.map +1 -1
  19. package/dist/components/ui/input.js +15 -9
  20. package/dist/components/ui/input.js.map +1 -1
  21. package/dist/hooks/content/useCreateImageContent.js +16 -22
  22. package/dist/hooks/content/useCreateImageContent.js.map +1 -1
  23. package/dist/hooks/storage/useImageUpload.js +36 -37
  24. package/dist/hooks/storage/useImageUpload.js.map +1 -1
  25. package/dist/hooks/util/useKeyboardAvoidingView.js +23 -0
  26. package/dist/hooks/util/useKeyboardAvoidingView.js.map +1 -0
  27. package/dist/index.js +218 -212
  28. package/dist/index.js.map +1 -1
  29. package/dist/mocks.js +4 -1
  30. package/dist/mocks.js.map +1 -1
  31. package/dist/shop-minis-platform/src/types/content.js.map +1 -1
  32. package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
  33. package/dist/shop-minis-react/node_modules/.pnpm/color-string@1.9.1/node_modules/color-string/index.js +1 -1
  34. 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
  35. package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
  36. package/dist/utils/colors.js +1 -1
  37. package/dist/utils/image.js +46 -9
  38. package/dist/utils/image.js.map +1 -1
  39. package/package.json +21 -4
  40. package/src/components/atoms/alert-dialog.test.tsx +67 -0
  41. package/src/components/atoms/alert-dialog.tsx +13 -11
  42. package/src/components/atoms/favorite-button.test.tsx +56 -0
  43. package/src/components/atoms/icon-button.tsx +1 -1
  44. package/src/components/atoms/image.test.tsx +108 -0
  45. package/src/components/atoms/{thumbhash-image.tsx → image.tsx} +14 -14
  46. package/src/components/atoms/product-variant-price.test.tsx +128 -0
  47. package/src/components/atoms/text-input.test.tsx +104 -0
  48. package/src/components/atoms/text-input.tsx +31 -0
  49. package/src/components/commerce/merchant-card.test.tsx +261 -0
  50. package/src/components/commerce/merchant-card.tsx +4 -2
  51. package/src/components/commerce/product-card.test.tsx +364 -0
  52. package/src/components/commerce/product-card.tsx +2 -2
  53. package/src/components/commerce/product-link.test.tsx +483 -0
  54. package/src/components/commerce/quantity-selector.test.tsx +382 -0
  55. package/src/components/commerce/search.test.tsx +487 -0
  56. package/src/components/content/image-content-wrapper.test.tsx +92 -0
  57. package/src/components/content/image-content-wrapper.tsx +9 -2
  58. package/src/components/index.ts +2 -1
  59. package/src/components/navigation/transition-link.test.tsx +155 -0
  60. package/src/components/ui/input.test.tsx +21 -0
  61. package/src/components/ui/input.tsx +10 -1
  62. package/src/hooks/content/useCreateImageContent.test.ts +352 -0
  63. package/src/hooks/content/useCreateImageContent.ts +1 -7
  64. package/src/hooks/index.ts +1 -0
  65. package/src/hooks/navigation/useNavigateWithTransition.test.ts +371 -0
  66. package/src/hooks/navigation/useViewTransitions.test.ts +469 -0
  67. package/src/hooks/product/useProductSearch.test.ts +470 -0
  68. package/src/hooks/storage/useAsyncStorage.test.ts +225 -0
  69. package/src/hooks/storage/useImageUpload.test.ts +322 -0
  70. package/src/hooks/storage/useImageUpload.ts +22 -20
  71. package/src/hooks/util/useKeyboardAvoidingView.ts +37 -0
  72. package/src/internal/useHandleAction.test.ts +265 -0
  73. package/src/internal/useShopActionsDataFetching.test.ts +465 -0
  74. package/src/mocks.ts +3 -1
  75. package/src/providers/ImagePickerProvider.test.tsx +467 -0
  76. package/src/stories/ProductCard.stories.tsx +2 -2
  77. package/src/stories/TextInput.stories.tsx +26 -0
  78. package/src/test-setup.ts +34 -0
  79. package/src/test-utils.tsx +167 -0
  80. package/src/utils/image.ts +73 -0
  81. package/src/utils/index.ts +1 -1
  82. package/dist/components/atoms/thumbhash-image.js +0 -54
  83. package/dist/components/atoms/thumbhash-image.js.map +0 -1
  84. package/dist/utils/imageToDataUri.js +0 -10
  85. package/dist/utils/imageToDataUri.js.map +0 -1
  86. 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/image'
6
+ import {getThumbhashDataURL, getResizedImageUrl} from '../../utils'
6
7
 
7
- type ThumbhashImageProps = ImgHTMLAttributes<HTMLImageElement> & {
8
- src: string
9
- thumbhash: string
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 ThumbhashImage = memo(function ThumbhashImage(
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 dataURL = useMemo(
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: dataURL ? `url(${dataURL})` : undefined,
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 w-full h-full opacity-0 object-cover',
57
+ 'absolute inset-0 opacity-0 object-cover',
57
58
  isLoaded && 'opacity-100'
58
59
  )}
59
- src={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 {ThumbhashImage} from '../atoms/thumbhash-image'
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
- <ThumbhashImage
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 (