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()
})
})
})