import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest' import { render, screen, userEvent, waitFor, mockProduct, mockMinisSDK, resetAllMocks, } from '../../test-utils' import {ProductLink} from './product-link' // 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('ProductLink', () => { beforeEach(() => { resetAllMocks() }) afterEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { it('renders product information correctly', () => { const product = mockProduct({ title: 'Test Product', price: {amount: '29.99', currencyCode: 'USD'}, }) render() expect(screen.getByText('Test Product')).toBeInTheDocument() expect(screen.getByText('$29.99')).toBeInTheDocument() }) it('renders product image when available', () => { const product = mockProduct({ featuredImage: { url: 'https://example.com/image.jpg', altText: 'Product Image', width: 100, height: 100, sensitive: false, thumbhash: null, }, }) render() const image = screen.getByRole('img') expect(image).toHaveAttribute('src', 'https://example.com/image.jpg') expect(image).toHaveAttribute('alt', 'Product Image') }) it('shows no image placeholder when image is not available', () => { const product = mockProduct({ featuredImage: undefined, }) render() expect(screen.getByText('No Image')).toBeInTheDocument() }) it('uses product title as alt text when image altText is missing', () => { const product = mockProduct({ title: 'My Product', featuredImage: { url: 'https://example.com/image.jpg', altText: '', width: 100, height: 100, sensitive: false, thumbhash: null, }, }) render() const image = screen.getByRole('img') expect(image).toHaveAttribute('alt', 'My Product') }) it('displays discount pricing when compareAtPrice exists', () => { const product = mockProduct({ price: {amount: '19.99', currencyCode: 'USD'}, compareAtPrice: {amount: '29.99', currencyCode: 'USD'}, }) render() // Discounted price should be shown expect(screen.getByText('$19.99')).toBeInTheDocument() // Original price with line-through const originalPrice = screen.getByText('$29.99') expect(originalPrice).toBeInTheDocument() expect(originalPrice).toHaveClass('line-through') }) it('shows regular price when no discount', () => { const product = mockProduct({ price: {amount: '29.99', currencyCode: 'USD'}, compareAtPrice: undefined, }) render() expect(screen.getByText('$29.99')).toBeInTheDocument() expect(screen.queryByText(/line-through/)).not.toBeInTheDocument() }) it('displays review rating and count when available', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 4.5, reviewCount: 123, }, }) render() // Should show review count expect(screen.getByText('(123)')).toBeInTheDocument() // Should show star rating const stars = document.querySelectorAll('svg') expect(stars.length).toBeGreaterThan(0) }) it('does not show rating when review data is missing', () => { const product = mockProduct({ reviewAnalytics: { averageRating: null, reviewCount: 0, }, }) render() expect( screen.queryByTestId('product-link-rating') ).not.toBeInTheDocument() }) it('shows favorite button by default', () => { const product = mockProduct() render() // Should have favorite button const buttons = screen.getAllByRole('button') expect(buttons.length).toBeGreaterThan(0) }) it('hides favorite button when hideFavoriteAction is true', () => { const product = mockProduct() render() // Should not have favorite button expect(screen.queryByRole('button')).not.toBeInTheDocument() }) it('renders custom action when provided', () => { const product = mockProduct() const onCustomActionClick = vi.fn() render( Add to Cart } onCustomActionClick={onCustomActionClick} /> ) // Custom action should be rendered expect(screen.getByTestId('custom-cta')).toBeInTheDocument() expect(screen.getByText('Add to Cart')).toBeInTheDocument() // Favorite button should not be rendered when custom action is present // Check that there's only one button (the custom one) const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(1) expect(buttons[0]).toHaveAttribute('data-testid', 'custom-cta') }) it('renders custom action even when hideFavoriteAction is true', () => { const product = mockProduct() const onCustomActionClick = vi.fn() render( Buy Now } onCustomActionClick={onCustomActionClick} /> ) // Custom action should still be rendered expect(screen.getByTestId('custom-action')).toBeInTheDocument() expect(screen.getByText('Buy Now')).toBeInTheDocument() }) }) describe('Interactions', () => { it('navigates to product on click', async () => { const user = userEvent.setup() const product = mockProduct() const onClick = vi.fn() render() const productElement = screen.getByText(product.title).closest('div') ?.parentElement?.parentElement await user.click(productElement!) expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({ productId: product.id, }) expect(onClick).toHaveBeenCalledWith(product) }) it('saves product when favorite button is clicked (unfavorited to favorited)', async () => { const user = userEvent.setup() const product = mockProduct({isFavorited: false}) 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, }) }) }) it('unsaves product when favorite button is clicked (favorited to unfavorited)', async () => { const user = userEvent.setup() const product = mockProduct({isFavorited: true}) 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, }) }) }) it('uses selected variant ID for favorite actions when available', async () => { const user = userEvent.setup() const product = mockProduct({ isFavorited: false, selectedVariant: { id: 'selected-variant-id', title: 'Selected Variant', isFavorited: false, availableForSale: true, price: {amount: '29.99', currencyCode: 'USD'}, compareAtPrice: null, image: null, }, }) 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: 'selected-variant-id', }) }) }) it('reverts favorite state on error', async () => { const user = userEvent.setup() const product = mockProduct({isFavorited: false}) // 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 and revert await waitFor(() => { // Should revert to unfilled state expect(favoriteButton).toHaveClass('bg-button-overlay/30') }) }) it('prevents event propagation on favorite button click', async () => { const user = userEvent.setup() const product = mockProduct() const onClick = vi.fn() mockMinisSDK.saveProduct.mockResolvedValue(undefined) render() const favoriteButton = screen.getByRole('button') await user.click(favoriteButton) // Should save product but not trigger navigation expect(mockMinisSDK.saveProduct).toHaveBeenCalled() expect(onClick).not.toHaveBeenCalled() expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled() }) it('calls onCustomActionClick when custom action is clicked', async () => { const user = userEvent.setup() const product = mockProduct() const onClick = vi.fn() const onCustomActionClick = vi.fn() render( Quick Add } onCustomActionClick={onCustomActionClick} /> ) const customButton = screen.getByTestId('custom-btn') await user.click(customButton) // Should call custom action handler expect(onCustomActionClick).toHaveBeenCalledTimes(1) // Should NOT call favorite save/unsave expect(mockMinisSDK.saveProduct).not.toHaveBeenCalled() expect(mockMinisSDK.unsaveProduct).not.toHaveBeenCalled() // Should NOT trigger product navigation expect(onClick).not.toHaveBeenCalled() expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled() }) }) describe('Star Rating Display', () => { it('displays correct number of filled stars based on rating', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 3.7, reviewCount: 50, }, }) render() // Check that rating is displayed expect(screen.getByText('(50)')).toBeInTheDocument() // Check for star icons (there's at least one star svg) const stars = document.querySelectorAll('svg') expect(stars.length).toBeGreaterThan(0) }) it('shows all empty stars for zero rating', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 0, reviewCount: 10, }, }) render() const stars = document.querySelectorAll('svg') const filledStars = Array.from(stars).filter( star => star.getAttribute('fill') === 'currentColor' ) expect(filledStars).toHaveLength(0) }) it('shows all filled stars for 5 star rating', () => { const product = mockProduct({ reviewAnalytics: { averageRating: 5, reviewCount: 100, }, }) render() // ProductReviewStars uses overlay technique with specific fill colors // Find the star container first to only count star SVGs const starContainer = document.querySelector( '[data-slot="product-review-stars"]' ) expect(starContainer).toBeInTheDocument() const stars = starContainer?.querySelectorAll('svg') || [] // For 5 star rating: 5 empty stars + 5 filled stars = 10 total expect(stars).toHaveLength(10) // Check that stars are rendered (5 filled stars using var(--grayscale-d100) style) const filledStars = Array.from(stars).filter(star => { const style = star.getAttribute('style') return style?.includes('var(--grayscale-d100)') }) expect(filledStars).toHaveLength(5) }) }) describe('Edge Cases', () => { it('handles product without price', () => { const product = mockProduct({ price: undefined, }) render() // Should still render without crashing expect(screen.getByText(product.title)).toBeInTheDocument() }) it('handles very long product titles', () => { const product = mockProduct({ title: 'This is a very long product title that should be truncated in the UI to prevent layout issues', }) render() // Title should be rendered and have truncate classes const titleElement = screen.getByText(product.title) expect(titleElement).toBeInTheDocument() expect(titleElement.className).toContain('text-ellipsis') }) it('handles rapid favorite toggling', async () => { const user = userEvent.setup() const product = mockProduct({isFavorited: false}) mockMinisSDK.saveProduct.mockResolvedValue(undefined) mockMinisSDK.unsaveProduct.mockResolvedValue(undefined) render() const favoriteButton = screen.getByRole('button') // Rapid clicks await user.click(favoriteButton) await user.click(favoriteButton) await user.click(favoriteButton) // Should handle all clicks gracefully expect(mockMinisSDK.saveProduct.mock.calls.length).toBeGreaterThan(0) }) it('handles product without shop data', () => { const product = mockProduct({ shop: {id: '', name: ''}, }) render() // Should still render expect(screen.getByText(product.title)).toBeInTheDocument() }) }) describe('Visual States', () => { it('applies hover/tap animation styles', () => { const product = mockProduct() const {container} = render() // Check that touchable wrapper exists const touchable = container.querySelector('[data-touchable="true"]') expect(touchable).toBeTruthy() }) it('shows correct favorite button state', async () => { const product = mockProduct({isFavorited: true}) render() const favoriteButton = screen.getByRole('button') expect(favoriteButton).toHaveClass('bg-primary') }) it('applies correct layout styles', () => { const product = mockProduct() render() const image = screen.getByRole('img') expect(image.parentElement).toHaveClass('h-16', 'w-16') }) }) describe('Accessibility', () => { it('has proper heading hierarchy', () => { const product = mockProduct() render() const heading = screen.getByRole('heading', {level: 3}) expect(heading).toHaveTextContent(product.title) }) it('provides proper alt text for images', () => { const product = mockProduct({ featuredImage: { url: 'https://example.com/image.jpg', altText: 'Descriptive alt text', width: 100, height: 100, sensitive: false, thumbhash: null, }, }) render() const image = screen.getByRole('img') expect(image).toHaveAttribute('alt', 'Descriptive alt text') }) }) })