import React from 'react';
import {render, act} from '@testing-library/react';
import {renderHook} from '@testing-library/react-hooks';
import {
EmbeddedCheckoutProvider,
useEmbeddedCheckoutContext,
} from './EmbeddedCheckoutProvider';
import * as mocks from '../../test/mocks';
describe('EmbeddedCheckoutProvider', () => {
let mockStripe: any;
let mockStripePromise: any;
let mockEmbeddedCheckout: any;
let mockEmbeddedCheckoutPromise: any;
const fakeClientSecret = 'cs_123_secret_abc';
const fetchClientSecret = () => Promise.resolve(fakeClientSecret);
const fakeOptions = {fetchClientSecret};
let consoleWarn: any;
let consoleError: any;
beforeEach(() => {
mockStripe = mocks.mockStripe();
mockStripePromise = Promise.resolve(mockStripe);
mockEmbeddedCheckout = mocks.mockEmbeddedCheckout();
mockEmbeddedCheckoutPromise = Promise.resolve(mockEmbeddedCheckout);
mockStripe.initEmbeddedCheckout.mockReturnValue(
mockEmbeddedCheckoutPromise
);
jest.spyOn(console, 'error');
jest.spyOn(console, 'warn');
consoleError = console.error;
consoleWarn = console.warn;
});
afterEach(() => {
jest.restoreAllMocks();
});
it('provides the Embedded Checkout instance via context', async () => {
const wrapper = ({children}: {children?: React.ReactNode}) => (
{children}
);
const {result} = renderHook(() => useEmbeddedCheckoutContext(), {wrapper});
await act(() => mockEmbeddedCheckoutPromise);
expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout);
});
it('only creates elements once', async () => {
const TestConsumerComponent = () => {
const _ = useEmbeddedCheckoutContext();
return
;
};
render(
);
await act(() => mockEmbeddedCheckoutPromise);
expect(mockStripe.initEmbeddedCheckout).toHaveBeenCalledTimes(1);
});
it('allows a transition from null to a valid Stripe object', async () => {
let stripeProp: any = null;
const wrapper = ({children}: {children?: React.ReactNode}) => (
{children}
);
const {result, rerender} = renderHook(() => useEmbeddedCheckoutContext(), {
wrapper,
});
expect(result.current.embeddedCheckout).toBe(null);
stripeProp = mockStripe;
rerender();
await act(() => mockEmbeddedCheckoutPromise);
expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout);
});
it('works with a Promise resolving to a valid Stripe object', async () => {
const wrapper = ({children}: {children?: React.ReactNode}) => (
{children}
);
const {result} = renderHook(() => useEmbeddedCheckoutContext(), {wrapper});
expect(result.current.embeddedCheckout).toBe(null);
await act(() => mockStripePromise);
await act(() => mockEmbeddedCheckoutPromise);
expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout);
});
it('allows a transition from null to a valid Promise', async () => {
let stripeProp: any = null;
const wrapper = ({children}: {children?: React.ReactNode}) => (
{children}
);
const {result, rerender} = renderHook(() => useEmbeddedCheckoutContext(), {
wrapper,
});
expect(result.current.embeddedCheckout).toBe(null);
stripeProp = mockStripePromise;
rerender();
expect(result.current.embeddedCheckout).toBe(null);
await act(() => mockStripePromise);
expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout);
});
it('works with a Promise resolving to null for SSR safety', async () => {
const nullPromise = Promise.resolve(null);
const TestConsumerComponent = () => {
const {embeddedCheckout} = useEmbeddedCheckoutContext();
return embeddedCheckout ? not empty
: null;
};
const {container} = render(
);
expect(container).toBeEmptyDOMElement();
await act(() => nullPromise.then(() => undefined));
expect(container).toBeEmptyDOMElement();
});
it('errors when props.stripe is `undefined`', () => {
// Silence console output so test output is less noisy
consoleError.mockImplementation(() => {});
expect(() =>
render(
)
).toThrow('Invalid prop `stripe` supplied to `EmbeddedCheckoutProvider`.');
});
it('errors when props.stripe is `false`', () => {
// Silence console output so test output is less noisy
consoleError.mockImplementation(() => {});
expect(() =>
render(
)
).toThrow('Invalid prop `stripe` supplied to `EmbeddedCheckoutProvider`.');
});
it('errors when props.stripe is a string', () => {
// Silence console output so test output is less noisy
consoleError.mockImplementation(() => {});
expect(() =>
render(
)
).toThrow('Invalid prop `stripe` supplied to `EmbeddedCheckoutProvider`.');
});
it('errors when props.stripe is a some other object', () => {
// Silence console output so test output is less noisy
consoleError.mockImplementation(() => {});
expect(() =>
render(
)
).toThrow('Invalid prop `stripe` supplied to `EmbeddedCheckoutProvider`.');
});
it('does not allow changes to a set Stripe object', async () => {
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
const {rerender} = render(
);
await act(() => mockEmbeddedCheckoutPromise);
const mockStripe2: any = mocks.mockStripe();
rerender(
);
expect(mockStripe.initEmbeddedCheckout.mock.calls).toHaveLength(1);
expect(mockStripe2.initEmbeddedCheckout.mock.calls).toHaveLength(0);
expect(consoleWarn).toHaveBeenCalledWith(
'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the `stripe` prop after setting it.'
);
});
describe('clientSecret param (deprecated)', () => {
it('allows a transition from null to a valid client secret', async () => {
let optionsProp: any = {clientSecret: null};
const wrapper = ({children}: {children?: React.ReactNode}) => (
{children}
);
const {result, rerender} = renderHook(
() => useEmbeddedCheckoutContext(),
{
wrapper,
}
);
expect(result.current.embeddedCheckout).toBe(null);
optionsProp = {clientSecret: fakeClientSecret};
rerender();
await act(() => mockEmbeddedCheckoutPromise);
expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout);
});
it('does not allow changes to clientSecret option', async () => {
const optionsProp1 = {clientSecret: 'cs_123_secret_abc'};
const optionsProp2 = {clientSecret: 'cs_abc_secret_123'};
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
const {rerender} = render(
);
await act(() => mockEmbeddedCheckoutPromise);
rerender(
);
expect(consoleWarn).toHaveBeenCalledWith(
'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the client secret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.'
);
});
});
describe('fetchClientSecret param', () => {
it('allows a transition from null to a valid fetchClientSecret', async () => {
let optionsProp: any = {fetchClientSecret: null};
const wrapper = ({children}: {children?: React.ReactNode}) => (
{children}
);
const {result, rerender} = renderHook(
() => useEmbeddedCheckoutContext(),
{
wrapper,
}
);
expect(result.current.embeddedCheckout).toBe(null);
optionsProp = {fetchClientSecret};
rerender();
await act(() => mockEmbeddedCheckoutPromise);
expect(result.current.embeddedCheckout).toBe(mockEmbeddedCheckout);
});
it('does not allow changes to fetchClientSecret option', async () => {
const optionsProp1 = {fetchClientSecret};
const optionsProp2 = {
fetchClientSecret: () => Promise.resolve('cs_abc_secret_123'),
};
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
const {rerender} = render(
);
await act(() => mockEmbeddedCheckoutPromise);
rerender(
);
expect(consoleWarn).toHaveBeenCalledWith(
'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change fetchClientSecret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.'
);
});
});
it('errors if both clientSecret and fetchClientSecret are undefined', async () => {
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
render(
);
expect(consoleWarn).toHaveBeenCalledWith(
'Invalid props passed to EmbeddedCheckoutProvider: You must provide one of either `options.fetchClientSecret` or `options.clientSecret`.'
);
});
it('does not allow changes to onComplete option', async () => {
const optionsProp1 = {
fetchClientSecret,
onComplete: () => 'foo',
};
const optionsProp2 = {
fetchClientSecret,
onComplete: () => 'bar',
};
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
const {rerender} = render(
);
await act(() => mockEmbeddedCheckoutPromise);
rerender(
);
expect(consoleWarn).toHaveBeenCalledWith(
'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the onComplete option after setting it.'
);
});
it('does not allow changes to onShippingDetailsChange option', async () => {
const optionsProp1 = {
fetchClientSecret,
onShippingDetailsChange: () => Promise.resolve({type: 'accept' as const}),
};
const optionsProp2 = {
fetchClientSecret,
onShippingDetailsChange: () => Promise.resolve({type: 'reject' as const}),
};
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
const {rerender} = render(
);
await act(() => mockEmbeddedCheckoutPromise);
rerender(
);
expect(consoleWarn).toHaveBeenCalledWith(
'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the onShippingDetailsChange option after setting it.'
);
});
it('does not allow changes to onLineItemsChange option', async () => {
const optionsProp1 = {
fetchClientSecret,
onLineItemsChange: () => Promise.resolve({type: 'accept' as const}),
};
const optionsProp2 = {
fetchClientSecret,
onLineItemsChange: () => Promise.resolve({type: 'reject' as const}),
};
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
const {rerender} = render(
);
await act(() => mockEmbeddedCheckoutPromise);
rerender(
);
expect(consoleWarn).toHaveBeenCalledWith(
'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the onLineItemsChange option after setting it.'
);
});
it('destroys Embedded Checkout when the component unmounts', async () => {
const {rerender} = render(
);
await act(() => mockEmbeddedCheckoutPromise);
rerender();
expect(mockEmbeddedCheckout.destroy).toBeCalled();
});
});