import {Product} from '@shopify/shop-minis-platform'
import {describe, expect, it, vi} from 'vitest'
import {
render,
screen,
mockMinisSDK,
resetAllMocks,
userEvent,
} from '../../test-utils'
import {AddToCartButton} from './add-to-cart'
// Mock hooks
vi.mock('../../internal/useShopCartActions', () => ({
useShopCartActions: () => ({
addToCart: mockMinisSDK.addToCart,
buyProduct: mockMinisSDK.buyProduct,
}),
}))
vi.mock('../../hooks', () => ({
useShopNavigation: () => ({
navigateToProduct: mockMinisSDK.navigateToProduct,
}),
useErrorToast: () => ({
showErrorToast: vi.fn(),
}),
}))
describe('AddToCartButton', () => {
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',
},
variants: [
{
id: 'gid://shopify/ProductVariant/456',
title: 'Default',
isFavorited: false,
availableForSale: true,
price: {
amount: '10.00',
currencyCode: 'USD',
},
image: {
url: 'https://example.com/variant-image.jpg',
altText: null,
width: null,
height: null,
},
},
],
}
const defaultProps = {
product: mockProduct,
productVariantId: 'gid://shopify/ProductVariant/456',
}
// eslint-disable-next-line jest/require-top-level-describe
beforeEach(() => {
resetAllMocks()
})
it('renders with default text', () => {
render()
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getByText('Add to cart')).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 codes prop', () => {
const discountCodes = ['SUMMER20', 'FREESHIP']
render()
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('calls addToCart when clicked and not a referral product', async () => {
const user = userEvent.setup()
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
render()
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
productId: mockProduct.id,
productVariantId: defaultProps.productVariantId,
quantity: 1,
discountCodes: undefined,
variantImageUrl: 'https://example.com/variant-image.jpg',
})
})
it('navigates to product page when product is referral', async () => {
const user = userEvent.setup()
const referralProduct: Product = {...mockProduct, referral: true}
render()
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.navigateToProduct).toHaveBeenCalledWith({
productId: referralProduct.id,
})
expect(mockMinisSDK.addToCart).not.toHaveBeenCalled()
})
it('shows success animation after adding to cart', async () => {
const user = userEvent.setup()
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
render()
const button = screen.getByRole('button')
await user.click(button)
// Check for success state (Added to cart text)
await screen.findByText('Added to cart')
})
it('handles add to cart error gracefully', async () => {
const user = userEvent.setup()
mockMinisSDK.addToCart.mockRejectedValueOnce(new Error('Failed'))
render()
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.addToCart).toHaveBeenCalled()
})
it('does not call addToCart when disabled', async () => {
const user = userEvent.setup()
render()
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.addToCart).not.toHaveBeenCalled()
})
it('handles product without variants array', async () => {
const user = userEvent.setup()
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
const productWithoutVariants: Product = {
...mockProduct,
variants: undefined,
}
render(
)
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
productId: productWithoutVariants.id,
productVariantId: 'gid://shopify/ProductVariant/456',
quantity: 1,
discountCodes: undefined,
variantImageUrl: undefined,
})
})
it('handles product without matching variant in array', async () => {
const user = userEvent.setup()
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
const productWithDifferentVariant: Product = {
...mockProduct,
variants: [
{
id: 'gid://shopify/ProductVariant/999',
title: 'Different',
isFavorited: false,
availableForSale: true,
price: {
amount: '15.00',
currencyCode: 'USD',
},
},
],
}
render(
)
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
productId: productWithDifferentVariant.id,
productVariantId: 'gid://shopify/ProductVariant/456',
quantity: 1,
discountCodes: undefined,
variantImageUrl: undefined,
})
})
it('handles product without shop data', async () => {
const user = userEvent.setup()
mockMinisSDK.addToCart.mockResolvedValueOnce({ok: true})
const productWithoutShop: Product = {
...mockProduct,
shop: undefined as any,
}
render(
)
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.addToCart).toHaveBeenCalledWith({
productId: productWithoutShop.id,
productVariantId: 'gid://shopify/ProductVariant/456',
quantity: 1,
discountCodes: undefined,
variantImageUrl: 'https://example.com/variant-image.jpg',
})
})
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('Add to cart')).not.toBeInTheDocument()
})
it('disables the button when matching variant is sold out', () => {
render(
)
expect(screen.getByRole('button')).toBeDisabled()
})
it('does not call addToCart when matching variant is sold out', async () => {
const user = userEvent.setup()
render(
)
const button = screen.getByRole('button')
await user.click(button)
expect(mockMinisSDK.addToCart).not.toHaveBeenCalled()
})
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.addToCart).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()
})
})
})