import React, {StrictMode} from 'react';
import {render, act} from '@testing-library/react';
import {renderHook} from '@testing-library/react-hooks';
import {Elements, useElements, ElementsConsumer} from './Elements';
import * as mocks from '../../test/mocks';
import {useStripe} from './useStripe';
describe('Elements', () => {
let mockStripe: any;
let mockStripePromise: any;
let mockElements: any;
let consoleError: any;
let consoleWarn: any;
beforeEach(() => {
mockStripe = mocks.mockStripe();
mockStripePromise = Promise.resolve(mockStripe);
mockElements = mocks.mockElements();
mockStripe.elements.mockReturnValue(mockElements);
jest.spyOn(console, 'error');
jest.spyOn(console, 'warn');
consoleError = console.error;
consoleWarn = console.warn;
});
afterEach(() => {
jest.restoreAllMocks();
});
test('injects elements with the useElements hook', () => {
const wrapper = ({children}: any) => (
{children}
);
const {result} = renderHook(() => useElements(), {wrapper});
expect(result.current).toBe(mockElements);
});
test('only creates elements once', () => {
const TestComponent = () => {
const _ = useElements();
return
;
};
render(
);
expect(mockStripe.elements).toHaveBeenCalledTimes(1);
});
test('injects stripe with the useStripe hook', () => {
const wrapper = ({children}: any) => (
{children}
);
const {result} = renderHook(() => useStripe(), {wrapper});
expect(result.current).toBe(mockStripe);
});
test('provides elements and stripe with the ElementsConsumer component', () => {
expect.assertions(2);
render(
{(ctx) => {
expect(ctx.elements).toBe(mockElements);
expect(ctx.stripe).toBe(mockStripe);
return null;
}}
);
});
test('provides given stripe instance on mount', () => {
const TestComponent = () => {
const stripe = useStripe();
if (!stripe) {
throw new Error('Stripe instance is null');
}
return null;
};
expect(() => {
render(
);
}).not.toThrow('Stripe instance is null');
});
test('allows a transition from null to a valid Stripe object', () => {
let stripeProp: any = null;
const wrapper = ({children}: any) => (
{children}
);
const {result, rerender} = renderHook(() => useElements(), {wrapper});
expect(result.current).toBe(null);
stripeProp = mockStripe;
rerender();
expect(result.current).toBe(mockElements);
});
test('works with a Promise resolving to a valid Stripe object', async () => {
const wrapper = ({children}: any) => (
{children}
);
const {result, waitForNextUpdate} = renderHook(() => useElements(), {
wrapper,
});
expect(result.current).toBe(null);
await waitForNextUpdate();
expect(result.current).toBe(mockElements);
});
test('allows a transition from null to a valid Promise', async () => {
let stripeProp: any = null;
const wrapper = ({children}: any) => (
{children}
);
const {result, rerender, waitForNextUpdate} = renderHook(
() => useElements(),
{wrapper}
);
expect(result.current).toBe(null);
stripeProp = mockStripePromise;
rerender();
expect(result.current).toBe(null);
await waitForNextUpdate();
expect(result.current).toBe(mockElements);
});
test('does not set context if Promise resolves after Elements is unmounted', async () => {
// Silence console output so test output is less noisy
consoleError.mockImplementation(() => {});
const {unmount} = render(
{null}
);
unmount();
await act(() => mockStripePromise);
expect(consoleError).not.toHaveBeenCalled();
});
test('works with a Promise resolving to null for SSR safety', async () => {
const nullPromise = Promise.resolve(null);
const TestComponent = () => {
const elements = useElements();
return elements ? not empty
: null;
};
const {container} = render(
);
expect(container).toBeEmptyDOMElement();
await act(() => nullPromise.then(() => undefined));
expect(container).toBeEmptyDOMElement();
});
test('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 `Elements`.'
);
});
test('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 `Elements`.'
);
});
test('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 `Elements`.'
);
});
test('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 `Elements`.'
);
});
test('does not allow changes to a set Stripe object', () => {
// Silence console output so test output is less noisy
consoleWarn.mockImplementation(() => {});
const {rerender} = render();
const mockStripe2: any = mocks.mockStripe();
rerender();
expect(mockStripe.elements.mock.calls).toHaveLength(1);
expect(mockStripe2.elements.mock.calls).toHaveLength(0);
expect(consoleWarn).toHaveBeenCalledWith(
'Unsupported prop change on Elements: You cannot change the `stripe` prop after setting it.'
);
});
test('allows changes to options via elements.update after setting the Stripe object', () => {
const {rerender} = render(
);
rerender();
expect(mockStripe.elements).toHaveBeenCalledWith({foo: 'foo'});
expect(mockStripe.elements).toHaveBeenCalledTimes(1);
expect(mockElements.update).toHaveBeenCalledWith({bar: 'bar'});
expect(mockStripe.elements).toHaveBeenCalledTimes(1);
});
test('allows options changes before setting the Stripe object', () => {
const {rerender} = render(
);
rerender();
expect(console.warn).not.toHaveBeenCalled();
rerender();
expect(mockStripe.elements).toHaveBeenCalledWith({bar: 'bar'});
});
test('throws when trying to call useElements outside of Elements context', () => {
const {result} = renderHook(() => useElements());
expect(result.error && result.error.message).toBe(
'Could not find Elements context; You need to wrap the part of your app that calls useElements() in an provider.'
);
});
test('throws when trying to call useStripe outside of Elements context', () => {
const {result} = renderHook(() => useStripe());
expect(result.error && result.error.message).toBe(
'Could not find Elements context; You need to wrap the part of your app that calls useStripe() in an provider.'
);
});
test('throws when trying to mount an outside of Elements context', () => {
// Silence console output so test output is less noisy
consoleError.mockImplementation(() => {});
const TestComponent = () => {
return {() => null};
};
expect(() => render()).toThrow(
'Could not find Elements context; You need to wrap the part of your app that mounts in an provider.'
);
});
describe('React.StrictMode', () => {
test('creates elements twice in StrictMode', () => {
const TestComponent = () => {
const _ = useElements();
return ;
};
render(
);
expect(mockStripe.elements).toHaveBeenCalledTimes(2);
});
test('allows changes to options via elements.update after setting the Stripe object in StrictMode', () => {
const TestComponent = () => {
const [options, setOptions] = React.useState({foo: 'foo'} as any);
React.useEffect(() => {
setOptions({bar: 'bar'} as any);
}, []);
return (
);
};
render();
expect(mockStripe.elements).toHaveBeenCalledWith({foo: 'foo'});
expect(mockStripe.elements).toHaveBeenCalledTimes(2);
expect(mockElements.update).toHaveBeenCalledWith({bar: 'bar'});
expect(mockStripe.elements).toHaveBeenCalledTimes(2);
});
test('creates only one elements instance when updated while resolving Stripe promise', async () => {
let updateResolver: any = () => {};
const updateResult = new Promise((resolve) => {
updateResolver = resolve;
});
let stripePromiseResolve: any = () => {};
const stripePromise = new Promise((resolve) => {
stripePromiseResolve = resolve;
});
// Only resolve stripe once the options have been updated
updateResult.then(() => {
stripePromiseResolve(mockStripePromise);
});
const TestComponent = () => {
const [_, forceRerender] = React.useState(0);
React.useEffect(() => {
setTimeout(() => {
forceRerender((val) => val + 1);
setTimeout(() => {
updateResolver();
});
});
}, []);
return (
);
};
render();
await act(async () => await updateResult);
await act(async () => await stripePromise);
expect(mockStripe.elements).toHaveBeenCalledWith({
appearance: {theme: 'flat'},
});
expect(mockStripe.elements).toHaveBeenCalledTimes(1);
});
test('creates only one elements instance when updated while resolving Stripe promise in StrictMode', async () => {
let updateResolver: any = () => {};
const updateResult = new Promise((resolve) => {
updateResolver = resolve;
});
let stripePromiseResolve: any = () => {};
const stripePromise = new Promise((resolve) => {
stripePromiseResolve = resolve;
});
// Only resolve stripe once the options have been updated
updateResult.then(() => {
stripePromiseResolve(mockStripePromise);
});
const TestComponent = () => {
const [_, forceRerender] = React.useState(0);
React.useEffect(() => {
setTimeout(() => {
forceRerender((val) => val + 1);
setTimeout(() => {
updateResolver();
});
});
}, []);
return (
);
};
render();
await act(async () => await updateResult);
await act(async () => await stripePromise);
expect(mockStripe.elements).toHaveBeenCalledWith({
appearance: {theme: 'flat'},
});
expect(mockStripe.elements).toHaveBeenCalledTimes(1);
});
});
});