import {Product} from '@shopify/shop-minis-platform' import {describe, expect, it, vi} from 'vitest' import { render, screen, mockMinisSDK, resetAllMocks, userEvent, waitFor, } from '../../test-utils' import {BuyNowButton} from './buy-now' // Mock hooks const mockShowErrorToast = vi.fn() vi.mock('../../internal/useShopCartActions', () => ({ useShopCartActions: () => ({ addToCart: mockMinisSDK.addToCart, buyProduct: mockMinisSDK.buyProduct, }), })) vi.mock('../../hooks', () => ({ useShopNavigation: () => ({ navigateToProduct: mockMinisSDK.navigateToProduct, }), useErrorToast: () => ({ showErrorToast: mockShowErrorToast, }), })) describe('BuyNowButton', () => { const mockProduct: Product = { id: 'gid://shopify/Product/123', title: 'Test Product', reviewAnalytics: { averageRating: null, reviewCount: null, }, shop: { id: 'gid://shopify/Shop/1', name: 'Test Shop', }, defaultVariantId: 'gid://shopify/ProductVariant/456', isFavorited: false, price: { amount: '10.00', currencyCode: 'USD', }, } const defaultProps = { product: mockProduct, productVariantId: 'gid://shopify/ProductVariant/456', } // eslint-disable-next-line jest/require-top-level-describe beforeEach(() => { resetAllMocks() mockShowErrorToast.mockClear() }) it('renders with default text', () => { render() expect(screen.getByRole('button')).toBeInTheDocument() expect(screen.getByText('Buy now')).toBeInTheDocument() }) it('renders with required props', () => { render() const button = screen.getByRole('button') expect(button).toBeInTheDocument() }) it('respects disabled prop', () => { render() const button = screen.getByRole('button') expect(button).toBeDisabled() }) it('applies custom className', () => { render() const button = screen.getByRole('button') expect(button).toHaveClass('custom-class') }) it('renders with different sizes', () => { const {rerender} = render() expect(screen.getByRole('button')).toBeInTheDocument() rerender() expect(screen.getByRole('button')).toBeInTheDocument() rerender() expect(screen.getByRole('button')).toBeInTheDocument() }) it('renders with discount code prop', () => { const discountCode = 'SUMMER20' render() expect(screen.getByRole('button')).toBeInTheDocument() }) it('calls buyProduct when clicked and not a referral product', async () => { const user = userEvent.setup() mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true}) render() const button = screen.getByRole('button') await user.click(button) expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({ productId: mockProduct.id, productVariantId: defaultProps.productVariantId, quantity: 1, discountCode: undefined, }) }) it('navigates to product page when product is referral', async () => { const user = userEvent.setup() const referralProduct: Product = {...mockProduct, referral: true} render() // Button should show "View product" instead of "Buy now" expect(screen.getByText('View product')).toBeInTheDocument() expect(screen.queryByText('Buy now')).not.toBeInTheDocument() const button = screen.getByRole('button') await user.click(button) expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({ productId: referralProduct.id, }) expect(mockMinisSDK.buyProduct).not.toHaveBeenCalled() }) it('shows processing state while purchasing', async () => { const user = userEvent.setup() mockMinisSDK.buyProduct.mockImplementation(() => new Promise(() => {})) // Never resolves render() const button = screen.getByRole('button') await user.click(button) // Check for processing state - button should be disabled and have aria-busy await waitFor(() => { expect(button).toBeDisabled() expect(button).toHaveAttribute('aria-busy', 'true') }) }) it('handles buy product error gracefully', async () => { const user = userEvent.setup() const error = new Error('Purchase failed') mockMinisSDK.buyProduct.mockRejectedValueOnce(error) render() const button = screen.getByRole('button') await user.click(button) await waitFor(() => { expect(mockMinisSDK.buyProduct).toHaveBeenCalled() expect(mockShowErrorToast).toHaveBeenCalledWith({ message: 'Failed to complete purchase', }) }) // Button should be enabled again after error expect(button).toBeEnabled() expect(button).toHaveAttribute('aria-busy', 'false') }) it('does not call buyProduct when disabled', async () => { const user = userEvent.setup() render() const button = screen.getByRole('button') await user.click(button) expect(mockMinisSDK.buyProduct).not.toHaveBeenCalled() }) it('passes discount code to buyProduct', async () => { const user = userEvent.setup() const discountCode = 'DISCOUNT10' mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true}) render() const button = screen.getByRole('button') await user.click(button) expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({ productId: mockProduct.id, productVariantId: defaultProps.productVariantId, quantity: 1, discountCode, }) }) it('handles product without shop data', async () => { const user = userEvent.setup() mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true}) const productWithoutShop: Product = { ...mockProduct, shop: undefined as any, } render( ) const button = screen.getByRole('button') await user.click(button) expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({ productId: productWithoutShop.id, productVariantId: 'gid://shopify/ProductVariant/456', quantity: 1, discountCode: undefined, }) }) it('handles product with partial shop data', async () => { const user = userEvent.setup() mockMinisSDK.buyProduct.mockResolvedValueOnce({ok: true}) const productWithPartialShop: Product = { ...mockProduct, shop: { id: 'gid://shopify/Shop/1', name: undefined as any, }, } render( ) const button = screen.getByRole('button') await user.click(button) expect(mockMinisSDK.buyProduct).toHaveBeenCalledWith({ productId: productWithPartialShop.id, productVariantId: 'gid://shopify/ProductVariant/456', quantity: 1, discountCode: undefined, }) }) describe('sold-out state', () => { const soldOutProduct: Product = { ...mockProduct, variants: [ { id: 'gid://shopify/ProductVariant/456', title: 'Default', isFavorited: false, availableForSale: false, price: { amount: '10.00', currencyCode: 'USD', }, }, ], } it('shows "Sold out" text when matching variant is not available for sale', () => { render( ) expect(screen.getByText('Sold out')).toBeInTheDocument() expect(screen.queryByText('Buy now')).not.toBeInTheDocument() }) it('referral products show "View product" and stay clickable even when sold out', async () => { const user = userEvent.setup() const referralSoldOutProduct: Product = { ...soldOutProduct, referral: true, } render( ) expect(screen.getByText('View product')).toBeInTheDocument() expect(screen.queryByText('Sold out')).not.toBeInTheDocument() const button = screen.getByRole('button') expect(button).toBeEnabled() await user.click(button) expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({ productId: referralSoldOutProduct.id, }) expect(mockMinisSDK.buyProduct).not.toHaveBeenCalled() }) it('falls back to selectedVariant when variants array is missing', () => { const productWithSelectedVariant: Product = { ...mockProduct, variants: undefined, selectedVariant: { id: 'gid://shopify/ProductVariant/456', title: 'Default', isFavorited: false, availableForSale: false, price: {amount: '10.00', currencyCode: 'USD'}, }, } render( ) expect(screen.getByText('Sold out')).toBeInTheDocument() expect(screen.getByRole('button')).toBeDisabled() }) it('disables the button when matching variant is sold out', () => { render( ) expect(screen.getByRole('button')).toBeDisabled() }) it('does not call buyProduct when matching variant is sold out', async () => { const user = userEvent.setup() render( ) const button = screen.getByRole('button') await user.click(button) expect(mockMinisSDK.buyProduct).not.toHaveBeenCalled() expect(mockMinisSDK.navigateToProduct).not.toHaveBeenCalled() }) }) })