Skip to content

Commit e06bf3b

Browse files
committed
test(shared): add unit tests for usePaymentElement hook
Cover the confirmSetup flow, error handling, reset, and provider readiness states to validate the Stripe SDK v9 upgrade.
1 parent 042fd15 commit e06bf3b

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import React from 'react';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
// --- Mock state ---
6+
7+
let mockStripe: any = null;
8+
let mockElements: any = null;
9+
const mockInitializePaymentMethod = vi.fn();
10+
11+
vi.mock('../../stripe-react', () => ({
12+
Elements: ({ children }: { children: React.ReactNode }) => <>{children}</>,
13+
PaymentElement: () => null,
14+
useElements: () => mockElements,
15+
useStripe: () => mockStripe,
16+
}));
17+
18+
vi.mock('../../hooks/useClerk', () => ({
19+
useClerk: () => ({
20+
__internal_getOption: () => undefined,
21+
__internal_environment: {
22+
commerceSettings: {
23+
billing: {
24+
stripePublishableKey: 'pk_test_123',
25+
},
26+
},
27+
displayConfig: {
28+
userProfileUrl: 'https://example.com/profile',
29+
organizationProfileUrl: 'https://example.com/org-profile',
30+
},
31+
},
32+
}),
33+
}));
34+
35+
vi.mock('../useInitializePaymentMethod', () => ({
36+
__internal_useInitializePaymentMethod: () => ({
37+
initializedPaymentMethod: {
38+
externalGatewayId: 'acct_123',
39+
externalClientSecret: 'seti_123',
40+
paymentMethodOrder: ['card'],
41+
},
42+
initializePaymentMethod: mockInitializePaymentMethod,
43+
}),
44+
}));
45+
46+
vi.mock('../useStripeClerkLibs', () => ({
47+
__internal_useStripeClerkLibs: () => ({
48+
loadStripe: vi.fn().mockResolvedValue({}),
49+
}),
50+
}));
51+
52+
vi.mock('../useStripeLoader', () => ({
53+
__internal_useStripeLoader: () => ({}),
54+
}));
55+
56+
const { __experimental_PaymentElementProvider, __experimental_usePaymentElement } = await import('../payment-element');
57+
58+
function createWrapper() {
59+
return function Wrapper({ children }: { children: React.ReactNode }) {
60+
return <__experimental_PaymentElementProvider>{children}</__experimental_PaymentElementProvider>;
61+
};
62+
}
63+
64+
describe('usePaymentElement', () => {
65+
beforeEach(() => {
66+
vi.clearAllMocks();
67+
mockStripe = null;
68+
mockElements = null;
69+
});
70+
71+
describe('when provider is not ready (no stripe/elements)', () => {
72+
it('returns isProviderReady=false and isFormReady=false', () => {
73+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
74+
wrapper: createWrapper(),
75+
});
76+
77+
expect(result.current.isProviderReady).toBe(false);
78+
expect(result.current.isFormReady).toBe(false);
79+
expect(result.current.provider).toBeUndefined();
80+
});
81+
82+
it('submit throws when stripe is not loaded', () => {
83+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
84+
wrapper: createWrapper(),
85+
});
86+
87+
expect(() => result.current.submit()).toThrow('Clerk: Unable to submit, Stripe libraries are not yet loaded');
88+
});
89+
90+
it('reset throws when stripe is not loaded', () => {
91+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
92+
wrapper: createWrapper(),
93+
});
94+
95+
expect(() => result.current.reset()).toThrow('Clerk: Unable to submit, Stripe libraries are not yet loaded');
96+
});
97+
});
98+
99+
describe('when provider is ready', () => {
100+
beforeEach(() => {
101+
mockStripe = {
102+
confirmSetup: vi.fn(),
103+
};
104+
mockElements = {
105+
create: vi.fn(),
106+
update: vi.fn(),
107+
};
108+
});
109+
110+
it('returns isProviderReady=true with stripe provider info', () => {
111+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
112+
wrapper: createWrapper(),
113+
});
114+
115+
expect(result.current.isProviderReady).toBe(true);
116+
expect(result.current.provider).toEqual({ name: 'stripe' });
117+
});
118+
119+
it('submit returns paymentToken on successful confirmSetup', async () => {
120+
mockStripe.confirmSetup.mockResolvedValue({
121+
setupIntent: { payment_method: 'pm_test_123' },
122+
error: null,
123+
});
124+
125+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
126+
wrapper: createWrapper(),
127+
});
128+
129+
let submitResult: any;
130+
await act(async () => {
131+
submitResult = await result.current.submit();
132+
});
133+
134+
expect(mockStripe.confirmSetup).toHaveBeenCalledWith({
135+
elements: mockElements,
136+
confirmParams: {
137+
return_url: window.location.href,
138+
},
139+
redirect: 'if_required',
140+
});
141+
expect(submitResult).toEqual({
142+
data: { gateway: 'stripe', paymentToken: 'pm_test_123' },
143+
error: null,
144+
});
145+
});
146+
147+
it('submit returns structured error when confirmSetup fails', async () => {
148+
mockStripe.confirmSetup.mockResolvedValue({
149+
setupIntent: null,
150+
error: {
151+
type: 'card_error',
152+
code: 'card_declined',
153+
message: 'Your card was declined.',
154+
},
155+
});
156+
157+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
158+
wrapper: createWrapper(),
159+
});
160+
161+
let submitResult: any;
162+
await act(async () => {
163+
submitResult = await result.current.submit();
164+
});
165+
166+
expect(submitResult).toEqual({
167+
data: null,
168+
error: {
169+
gateway: 'stripe',
170+
error: {
171+
type: 'card_error',
172+
code: 'card_declined',
173+
message: 'Your card was declined.',
174+
},
175+
},
176+
});
177+
});
178+
179+
it('submit handles validation_error type from confirmSetup', async () => {
180+
mockStripe.confirmSetup.mockResolvedValue({
181+
setupIntent: null,
182+
error: {
183+
type: 'validation_error',
184+
message: 'Your card number is incomplete.',
185+
},
186+
});
187+
188+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
189+
wrapper: createWrapper(),
190+
});
191+
192+
let submitResult: any;
193+
await act(async () => {
194+
submitResult = await result.current.submit();
195+
});
196+
197+
expect(submitResult).toEqual({
198+
data: null,
199+
error: {
200+
gateway: 'stripe',
201+
error: {
202+
type: 'validation_error',
203+
code: undefined,
204+
message: 'Your card number is incomplete.',
205+
},
206+
},
207+
});
208+
});
209+
210+
it('reset calls initializePaymentMethod', async () => {
211+
mockInitializePaymentMethod.mockResolvedValue({
212+
externalClientSecret: 'seti_new_456',
213+
gateway: 'stripe',
214+
});
215+
216+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
217+
wrapper: createWrapper(),
218+
});
219+
220+
await act(async () => {
221+
await result.current.reset();
222+
});
223+
224+
expect(mockInitializePaymentMethod).toHaveBeenCalledTimes(1);
225+
});
226+
});
227+
});

0 commit comments

Comments
 (0)