@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,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
|
+
})
|