Skip to content

Commit 4f86c47

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 4f86c47

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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(
57+
'../payment-element'
58+
);
59+
60+
function createWrapper() {
61+
return function Wrapper({ children }: { children: React.ReactNode }) {
62+
return <__experimental_PaymentElementProvider>{children}</__experimental_PaymentElementProvider>;
63+
};
64+
}
65+
66+
describe('usePaymentElement', () => {
67+
beforeEach(() => {
68+
vi.clearAllMocks();
69+
mockStripe = null;
70+
mockElements = null;
71+
});
72+
73+
describe('when provider is not ready (no stripe/elements)', () => {
74+
it('returns isProviderReady=false and isFormReady=false', () => {
75+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
76+
wrapper: createWrapper(),
77+
});
78+
79+
expect(result.current.isProviderReady).toBe(false);
80+
expect(result.current.isFormReady).toBe(false);
81+
expect(result.current.provider).toBeUndefined();
82+
});
83+
84+
it('submit throws when stripe is not loaded', () => {
85+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
86+
wrapper: createWrapper(),
87+
});
88+
89+
expect(() => result.current.submit()).toThrow(
90+
'Clerk: Unable to submit, Stripe libraries are not yet loaded',
91+
);
92+
});
93+
94+
it('reset throws when stripe is not loaded', () => {
95+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
96+
wrapper: createWrapper(),
97+
});
98+
99+
expect(() => result.current.reset()).toThrow(
100+
'Clerk: Unable to submit, Stripe libraries are not yet loaded',
101+
);
102+
});
103+
});
104+
105+
describe('when provider is ready', () => {
106+
beforeEach(() => {
107+
mockStripe = {
108+
confirmSetup: vi.fn(),
109+
};
110+
mockElements = {
111+
create: vi.fn(),
112+
update: vi.fn(),
113+
};
114+
});
115+
116+
it('returns isProviderReady=true with stripe provider info', () => {
117+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
118+
wrapper: createWrapper(),
119+
});
120+
121+
expect(result.current.isProviderReady).toBe(true);
122+
expect(result.current.provider).toEqual({ name: 'stripe' });
123+
});
124+
125+
it('submit returns paymentToken on successful confirmSetup', async () => {
126+
mockStripe.confirmSetup.mockResolvedValue({
127+
setupIntent: { payment_method: 'pm_test_123' },
128+
error: null,
129+
});
130+
131+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
132+
wrapper: createWrapper(),
133+
});
134+
135+
let submitResult: any;
136+
await act(async () => {
137+
submitResult = await result.current.submit();
138+
});
139+
140+
expect(mockStripe.confirmSetup).toHaveBeenCalledWith({
141+
elements: mockElements,
142+
confirmParams: {
143+
return_url: window.location.href,
144+
},
145+
redirect: 'if_required',
146+
});
147+
expect(submitResult).toEqual({
148+
data: { gateway: 'stripe', paymentToken: 'pm_test_123' },
149+
error: null,
150+
});
151+
});
152+
153+
it('submit returns structured error when confirmSetup fails', async () => {
154+
mockStripe.confirmSetup.mockResolvedValue({
155+
setupIntent: null,
156+
error: {
157+
type: 'card_error',
158+
code: 'card_declined',
159+
message: 'Your card was declined.',
160+
},
161+
});
162+
163+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
164+
wrapper: createWrapper(),
165+
});
166+
167+
let submitResult: any;
168+
await act(async () => {
169+
submitResult = await result.current.submit();
170+
});
171+
172+
expect(submitResult).toEqual({
173+
data: null,
174+
error: {
175+
gateway: 'stripe',
176+
error: {
177+
type: 'card_error',
178+
code: 'card_declined',
179+
message: 'Your card was declined.',
180+
},
181+
},
182+
});
183+
});
184+
185+
it('submit handles validation_error type from confirmSetup', async () => {
186+
mockStripe.confirmSetup.mockResolvedValue({
187+
setupIntent: null,
188+
error: {
189+
type: 'validation_error',
190+
message: 'Your card number is incomplete.',
191+
},
192+
});
193+
194+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
195+
wrapper: createWrapper(),
196+
});
197+
198+
let submitResult: any;
199+
await act(async () => {
200+
submitResult = await result.current.submit();
201+
});
202+
203+
expect(submitResult).toEqual({
204+
data: null,
205+
error: {
206+
gateway: 'stripe',
207+
error: {
208+
type: 'validation_error',
209+
code: undefined,
210+
message: 'Your card number is incomplete.',
211+
},
212+
},
213+
});
214+
});
215+
216+
it('reset calls initializePaymentMethod', async () => {
217+
mockInitializePaymentMethod.mockResolvedValue({
218+
externalClientSecret: 'seti_new_456',
219+
gateway: 'stripe',
220+
});
221+
222+
const { result } = renderHook(() => __experimental_usePaymentElement(), {
223+
wrapper: createWrapper(),
224+
});
225+
226+
await act(async () => {
227+
await result.current.reset();
228+
});
229+
230+
expect(mockInitializePaymentMethod).toHaveBeenCalledTimes(1);
231+
});
232+
});
233+
});

0 commit comments

Comments
 (0)