import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest' import { render, screen, userEvent, waitFor, mockProduct, mockProductVariant, mockMinisSDK, resetAllMocks, } from '../../test-utils' import { ProductCard, ProductCardContainer, ProductCardImageContainer, ProductCardImage, ProductCardBadge, ProductCardFavoriteButton, ProductCardInfo, ProductCardTitle, ProductCardReviewStars, ProductCardPrice, } from './product-card' // Mock hooks vi.mock('../../hooks/navigation/useShopNavigation', () => ({ useShopNavigation: () => ({ navigateToProduct: mockMinisSDK.navigateToProduct, navigateToShop: mockMinisSDK.navigateToShop, }), })) vi.mock('../../hooks/user/useSavedProductsActions', () => ({ useSavedProductsActions: () => ({ saveProduct: mockMinisSDK.saveProduct, unsaveProduct: mockMinisSDK.unsaveProduct, }), })) // Mock formatMoney vi.mock('../../utils/formatMoney', () => ({ formatMoney: vi.fn((amount: string, currencyCode: string) => { const numAmount = parseFloat(amount) return currencyCode === 'USD' ? `$${numAmount.toFixed(2)}` : `${currencyCode} ${numAmount.toFixed(2)}` }), })) describe('ProductCard', () => { beforeEach(() => { resetAllMocks() }) afterEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { it('renders with default variant', () => { const product = mockProduct() render() // Check for product title expect(screen.getByText(product.title)).toBeInTheDocument() // Check for price (default from createProduct is $99.99) expect(screen.getByText('$99.99')).toBeInTheDocument() // Check for image container expect(screen.getByRole('img')).toBeInTheDocument() }) it('renders with priceOverlay variant', () => { const product = mockProduct() render() // Price should be in a badge on top-left const badges = screen.getAllByText('$99.99') expect(badges.length).toBeGreaterThan(0) // Should not show product info section in priceOverlay variant expect(screen.queryByText(product.title)).toBeNull() }) it('renders with compact variant', () => { const product = mockProduct() render() // Should not show product info section in compact variant const title = screen.queryByText(product.title) expect(title).toBeNull() }) it('renders with selected variant', () => { const product = mockProduct() const selectedVariant = mockProductVariant({ id: 'variant-2', price: {amount: '39.99', currencyCode: 'USD'}, }) render( ) // Should show selected variant price expect(screen.getByText('$39.99')).toBeInTheDocument() }) it('renders with compare at price', () => { const product = mockProduct({ price: {amount: '29.99', currencyCode: 'USD'}, compareAtPrice: {amount: '39.99', currencyCode: 'USD'}, }) render() // Should show both prices expect(screen.getByText('$29.99')).toBeInTheDocument() expect(screen.getByText('$39.99')).toBeInTheDocument() // Compare at price should have line-through const compareAtPrice = screen.getByText('$39.99') expect(compareAtPrice).toHaveClass('line-through') }) it('renders badge when provided', () => { const product = mockProduct() render( ) expect(screen.getByText('Sale')).toBeInTheDocument() }) it('renders without image when product has no featured image', () => { const product = mockProduct({featuredImage: undefined}) render() expect(screen.getByText('No Image')).toBeInTheDocument() }) it('renders favorite button by default', () => { const product = mockProduct() render() // Favorite button should be present const favoriteButton = screen.getByRole('button') expect(favoriteButton).toBeInTheDocument() }) it('hides favorite button when favoriteButtonDisabled is true', () => { const product = mockProduct() render() // Favorite button should not be present const favoriteButton = screen.queryByRole('button') expect(favoriteButton).not.toBeInTheDocument() }) it('shows favorite button when favoriteButtonDisabled is false', () => { const product = mockProduct() render() // Favorite button should be present const favoriteButton = screen.getByRole('button') expect(favoriteButton).toBeInTheDocument() }) it('renders review stars when review data is available', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 4.5, reviewCount: 123, }, }) render() // Should show the review stars and count expect(screen.getByText('(123)')).toBeInTheDocument() // Check for star elements - ProductReviewStars uses overlay technique with 10 stars total const container = screen.getByText('(123)').parentElement const stars = container?.querySelectorAll('svg') // 5 empty stars + filled stars overlay (4 full + 1 half for 4.5 rating) expect(stars).toHaveLength(10) // Verify review count is displayed expect(screen.getByText('(123)')).toBeInTheDocument() }) it('does not render review stars when review data is missing', () => { const product = mockProduct({ reviewAnalytics: undefined, }) render() // Should not show review count expect(screen.queryByText(/^\(\d+\)$/)).not.toBeInTheDocument() }) it('does not render review stars when averageRating is null', () => { const product = mockProduct({ reviewAnalytics: { averageRating: null, reviewCount: 100, }, }) render() // Should not show review count expect(screen.queryByText('(100)')).not.toBeInTheDocument() }) it('does not render review stars when reviewCount is 0', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 4.5, reviewCount: 0, }, }) render() // Should not show review count expect(screen.queryByText('(0)')).not.toBeInTheDocument() }) it('formats large review counts correctly', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 4.0, reviewCount: 1500, }, }) render() // Should format as 1K expect(screen.getByText('(1K)')).toBeInTheDocument() }) it('does not show review stars in non-default variants', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 4.5, reviewCount: 123, }, }) const {rerender} = render( ) // Should not show review stars in priceOverlay variant expect(screen.queryByText('(123)')).not.toBeInTheDocument() rerender() // Should not show review stars in compact variant expect(screen.queryByText('(123)')).not.toBeInTheDocument() rerender() // Should show review stars in default variant expect(screen.getByText('(123)')).toBeInTheDocument() }) }) describe('Interactions', () => { it('handles product click navigation', async () => { const user = userEvent.setup() const product = mockProduct() const onProductClick = vi.fn() render() const container = screen.getByText(product.title).closest('div') ?.parentElement?.parentElement await user.click(container!) expect(onProductClick).toHaveBeenCalledTimes(1) expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({ productId: product.id, }) }) it('does not navigate when touchable is false', async () => { const user = userEvent.setup() const product = mockProduct() const onProductClick = vi.fn() render( ) const container = screen.getByText(product.title).closest('div') ?.parentElement?.parentElement await user.click(container!) expect(onProductClick).not.toHaveBeenCalled() expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled() }) it('handles favorite toggle - save product', async () => { const user = userEvent.setup() const product = mockProduct({isFavorited: false}) const onFavoriteToggled = vi.fn() mockMinisSDK.saveProduct.mockResolvedValue(undefined) render( ) const favoriteButton = screen.getByRole('button') await user.click(favoriteButton) await waitFor(() => { expect(mockMinisSDK.saveProduct).toHaveBeenCalledWith({ productId: product.id, shopId: product.shop.id, productVariantId: product.defaultVariantId, }) expect(onFavoriteToggled).toHaveBeenCalledWith(true) }) }) it('handles favorite toggle - unsave product', async () => { const user = userEvent.setup() const product = mockProduct({isFavorited: true}) const onFavoriteToggled = vi.fn() mockMinisSDK.unsaveProduct.mockResolvedValue(undefined) render( ) const favoriteButton = screen.getByRole('button') await user.click(favoriteButton) await waitFor(() => { expect(mockMinisSDK.unsaveProduct).toHaveBeenCalledWith({ productId: product.id, shopId: product.shop.id, productVariantId: product.defaultVariantId, }) expect(onFavoriteToggled).toHaveBeenCalledWith(false) }) }) it('reverts favorite state on error', async () => { const user = userEvent.setup() const product = mockProduct({isFavorited: false}) const onFavoriteToggled = vi.fn() // Mock save to reject mockMinisSDK.saveProduct.mockRejectedValue(new Error('Save failed')) render( ) const favoriteButton = screen.getByRole('button') // Initially unfilled expect(favoriteButton).toHaveClass('bg-button-overlay/30') await user.click(favoriteButton) // Should call save expect(mockMinisSDK.saveProduct).toHaveBeenCalled() // Wait for error handling await waitFor(() => { // Should revert to unfilled state expect(favoriteButton).toHaveClass('bg-button-overlay/30') }) }) it('does not allow favorite toggle when favoriteButtonDisabled is true', async () => { const product = mockProduct({isFavorited: false}) const onFavoriteToggled = vi.fn() render( ) // Button should not exist at all const favoriteButton = screen.queryByRole('button') expect(favoriteButton).not.toBeInTheDocument() // Callbacks should not be called expect(onFavoriteToggled).not.toHaveBeenCalled() expect(mockMinisSDK.saveProduct).not.toHaveBeenCalled() expect(mockMinisSDK.unsaveProduct).not.toHaveBeenCalled() }) }) describe('Custom Composition', () => { it('renders with custom children layout', () => { const product = mockProduct() render(
Custom Title
) expect(screen.getByTestId('custom-layout')).toBeInTheDocument() expect(screen.getByText('Custom Title')).toBeInTheDocument() expect(screen.getByText('$99.99')).toBeInTheDocument() }) it('allows individual component usage', () => { const product = mockProduct() render( Custom Badge ) expect(screen.getByText('Custom Badge')).toBeInTheDocument() expect(screen.getByText(product.title)).toBeInTheDocument() expect(screen.getByText('$99.99')).toBeInTheDocument() }) it('allows ProductCardReviewStars in custom composition', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 4.2, reviewCount: 50, }, }) render( ) // Review stars should be rendered expect(screen.getByText('(50)')).toBeInTheDocument() // Check for star elements - ProductReviewStars uses overlay technique const container = screen.getByText('(50)').parentElement const stars = container?.querySelectorAll('svg') expect(stars).toHaveLength(9) // 5 empty + 3 full + 1 half for 3.5 rating }) it('respects favoriteButtonDisabled in custom composition', () => { const product = mockProduct() render( ) // Favorite button should not be rendered even when explicitly included const favoriteButton = screen.queryByRole('button') expect(favoriteButton).not.toBeInTheDocument() }) }) describe('Edge Cases', () => { it('handles product without variants', () => { const product = mockProduct({ variants: [], selectedVariant: undefined, }) render() expect(screen.getByText(product.title)).toBeInTheDocument() expect(screen.getByText('$99.99')).toBeInTheDocument() }) }) describe('Accessibility', () => { it('has proper alt text for images', () => { const product = mockProduct() render() const img = screen.getByRole('img') expect(img).toHaveAttribute( 'alt', product.featuredImage?.altText || product.title ) }) it('uses product title as alt when image alt text is missing', () => { const product = mockProduct({ featuredImage: { url: 'https://example.com/image.jpg', altText: '', width: 1000, height: 1000, sensitive: false, thumbhash: 'thumbhash', }, }) render() const img = screen.getByRole('img') expect(img).toHaveAttribute('alt', product.title) }) it('maintains proper heading hierarchy', () => { const product = mockProduct() render() const heading = screen.getByRole('heading', {level: 3}) expect(heading).toHaveTextContent(product.title) }) }) })