@startsimpli/billing 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -0
- package/package.json +50 -0
- package/src/components/ManageSubscription.test.tsx +140 -0
- package/src/components/ManageSubscription.tsx +53 -0
- package/src/components/PricingDetailPage.integration.test.tsx +244 -0
- package/src/components/PricingDetailPage.test.tsx +404 -0
- package/src/components/PricingDetailPage.tsx +295 -0
- package/src/components/PricingPage.test.tsx +278 -0
- package/src/components/PricingPage.tsx +153 -0
- package/src/components/PricingSection.test.tsx +319 -0
- package/src/components/PricingSection.tsx +154 -0
- package/src/components/SubscriptionManager.test.tsx +498 -0
- package/src/components/SubscriptionManager.tsx +270 -0
- package/src/components/UpgradeModal.test.tsx +152 -0
- package/src/components/UpgradeModal.tsx +195 -0
- package/src/components/index.ts +12 -0
- package/src/hooks/BillingProvider.test.tsx +125 -0
- package/src/hooks/BillingProvider.tsx +52 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useCheckout.test.tsx +232 -0
- package/src/hooks/useCheckout.ts +75 -0
- package/src/hooks/usePortal.test.tsx +189 -0
- package/src/hooks/usePortal.ts +43 -0
- package/src/hooks/useProduct.test.tsx +155 -0
- package/src/hooks/useProduct.ts +43 -0
- package/src/hooks/useSubscription.test.tsx +167 -0
- package/src/hooks/useSubscription.ts +40 -0
- package/src/index.ts +47 -0
- package/src/server/index.ts +2 -0
- package/src/server/proxy.ts +89 -0
- package/src/types/index.ts +78 -0
- package/src/utils/api.test.ts +129 -0
- package/src/utils/api.ts +123 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD tests for SubscriptionManager component.
|
|
3
|
+
*
|
|
4
|
+
* SubscriptionManager is an interactive subscription management widget
|
|
5
|
+
* for logged-in users. It displays the current subscription, allows
|
|
6
|
+
* upgrade/downgrade options, and provides portal access.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <SubscriptionManager
|
|
10
|
+
* productId="raise-simpli"
|
|
11
|
+
* returnUrl="https://app.example.com/settings"
|
|
12
|
+
* onPlanChange={(offer) => console.log(offer)}
|
|
13
|
+
* />
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, vi } from "vitest";
|
|
17
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
18
|
+
import userEvent from "@testing-library/user-event";
|
|
19
|
+
import React from "react";
|
|
20
|
+
import { BillingProvider } from "../hooks/BillingProvider";
|
|
21
|
+
import { SubscriptionManager } from "./SubscriptionManager";
|
|
22
|
+
import type { BillingProduct, SubscriptionInfo } from "../types";
|
|
23
|
+
|
|
24
|
+
const mockProduct: BillingProduct = {
|
|
25
|
+
id: "prod-1",
|
|
26
|
+
slug: "raise-simpli",
|
|
27
|
+
name: "RaiseSimpli",
|
|
28
|
+
description: "VC fundraising platform",
|
|
29
|
+
offers: [
|
|
30
|
+
{
|
|
31
|
+
id: "offer-1",
|
|
32
|
+
name: "Starter",
|
|
33
|
+
slug: "starter-monthly",
|
|
34
|
+
unit_price: "0.00",
|
|
35
|
+
currency: "USD",
|
|
36
|
+
pricing_model: "flat",
|
|
37
|
+
interval_unit: "month",
|
|
38
|
+
interval_count: 1,
|
|
39
|
+
interval_display: "per month",
|
|
40
|
+
is_recurring: true,
|
|
41
|
+
is_free: true,
|
|
42
|
+
features: [
|
|
43
|
+
{ key: "seats", name: "Team seats", value: 1, limit: 1 },
|
|
44
|
+
{ key: "projects", name: "Projects", value: 3, limit: 3 },
|
|
45
|
+
],
|
|
46
|
+
is_active: true,
|
|
47
|
+
is_featured: false,
|
|
48
|
+
trial_days: 0,
|
|
49
|
+
sort_order: 0,
|
|
50
|
+
cta_text: "Start Free",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "offer-2",
|
|
54
|
+
name: "Pro",
|
|
55
|
+
slug: "pro-monthly",
|
|
56
|
+
unit_price: "49.00",
|
|
57
|
+
currency: "USD",
|
|
58
|
+
pricing_model: "flat",
|
|
59
|
+
interval_unit: "month",
|
|
60
|
+
interval_count: 1,
|
|
61
|
+
interval_display: "per month",
|
|
62
|
+
is_recurring: true,
|
|
63
|
+
is_free: false,
|
|
64
|
+
features: [
|
|
65
|
+
{ key: "seats", name: "Team seats", value: 5, limit: 5 },
|
|
66
|
+
{ key: "projects", name: "Projects", value: "Unlimited" },
|
|
67
|
+
],
|
|
68
|
+
is_active: true,
|
|
69
|
+
is_featured: true,
|
|
70
|
+
trial_days: 14,
|
|
71
|
+
sort_order: 1,
|
|
72
|
+
cta_text: "Start Trial",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "offer-3",
|
|
76
|
+
name: "Enterprise",
|
|
77
|
+
slug: "enterprise-monthly",
|
|
78
|
+
unit_price: "199.00",
|
|
79
|
+
currency: "USD",
|
|
80
|
+
pricing_model: "per_seat",
|
|
81
|
+
interval_unit: "month",
|
|
82
|
+
interval_count: 1,
|
|
83
|
+
interval_display: "per month",
|
|
84
|
+
is_recurring: true,
|
|
85
|
+
is_free: false,
|
|
86
|
+
features: [
|
|
87
|
+
{ key: "seats", name: "Team seats", value: "Unlimited" },
|
|
88
|
+
{ key: "projects", name: "Projects", value: "Unlimited" },
|
|
89
|
+
],
|
|
90
|
+
is_active: true,
|
|
91
|
+
is_featured: false,
|
|
92
|
+
trial_days: 0,
|
|
93
|
+
sort_order: 2,
|
|
94
|
+
cta_text: "Contact Sales",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mockSubscriptionActive: SubscriptionInfo = {
|
|
100
|
+
id: "sub-123",
|
|
101
|
+
offer: mockProduct.offers[1], // Pro plan
|
|
102
|
+
status: "active",
|
|
103
|
+
current_period_start: "2026-01-01T00:00:00Z",
|
|
104
|
+
current_period_end: "2026-02-01T00:00:00Z",
|
|
105
|
+
cancel_at_period_end: false,
|
|
106
|
+
trial_end: null,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const mockSubscriptionTrialing: SubscriptionInfo = {
|
|
110
|
+
id: "sub-456",
|
|
111
|
+
offer: mockProduct.offers[1], // Pro plan
|
|
112
|
+
status: "trialing",
|
|
113
|
+
current_period_start: "2026-01-01T00:00:00Z",
|
|
114
|
+
current_period_end: "2026-02-01T00:00:00Z",
|
|
115
|
+
cancel_at_period_end: false,
|
|
116
|
+
trial_end: "2026-01-15T00:00:00Z",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const mockSubscriptionPastDue: SubscriptionInfo = {
|
|
120
|
+
id: "sub-789",
|
|
121
|
+
offer: mockProduct.offers[1], // Pro plan
|
|
122
|
+
status: "past_due",
|
|
123
|
+
current_period_start: "2026-01-01T00:00:00Z",
|
|
124
|
+
current_period_end: "2026-02-01T00:00:00Z",
|
|
125
|
+
cancel_at_period_end: false,
|
|
126
|
+
trial_end: null,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const mockSubscriptionCancelled: SubscriptionInfo = {
|
|
130
|
+
id: "sub-999",
|
|
131
|
+
offer: mockProduct.offers[1], // Pro plan
|
|
132
|
+
status: "cancelled",
|
|
133
|
+
current_period_start: "2026-01-01T00:00:00Z",
|
|
134
|
+
current_period_end: "2026-02-01T00:00:00Z",
|
|
135
|
+
cancel_at_period_end: true,
|
|
136
|
+
trial_end: null,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function createWrapper(fetcher: typeof fetch) {
|
|
140
|
+
return function Wrapper({ children }: { children: React.ReactNode }) {
|
|
141
|
+
return (
|
|
142
|
+
<BillingProvider
|
|
143
|
+
apiBaseUrl="https://api.test.com/api/v1"
|
|
144
|
+
authToken="tok_123"
|
|
145
|
+
fetcher={fetcher}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</BillingProvider>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mockFetchRouter(routes: Record<string, any>) {
|
|
154
|
+
return vi.fn((url: string) => {
|
|
155
|
+
// Match subscription endpoint
|
|
156
|
+
if (url.includes("/billing/subscription/current/")) {
|
|
157
|
+
const response = routes["/billing/subscription/current/"];
|
|
158
|
+
if (response === null) {
|
|
159
|
+
return Promise.resolve({
|
|
160
|
+
ok: false,
|
|
161
|
+
status: 404,
|
|
162
|
+
json: () => Promise.resolve({ detail: "No subscription found" }),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (response instanceof Error) {
|
|
166
|
+
return Promise.resolve({
|
|
167
|
+
ok: false,
|
|
168
|
+
status: 500,
|
|
169
|
+
json: () => Promise.resolve({ detail: response.message }),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return Promise.resolve({
|
|
173
|
+
ok: true,
|
|
174
|
+
json: () => Promise.resolve(response),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Match product endpoint
|
|
179
|
+
if (url.includes("/billing/products/")) {
|
|
180
|
+
const response = routes["/billing/products/"];
|
|
181
|
+
return Promise.resolve({
|
|
182
|
+
ok: true,
|
|
183
|
+
json: () => Promise.resolve(response),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Promise.resolve({
|
|
188
|
+
ok: false,
|
|
189
|
+
status: 404,
|
|
190
|
+
});
|
|
191
|
+
}) as unknown as typeof fetch;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
describe("SubscriptionManager", () => {
|
|
195
|
+
it("renders loading state while fetching subscription", () => {
|
|
196
|
+
// Never resolves — stays in loading
|
|
197
|
+
const fetcher = vi.fn().mockReturnValue(
|
|
198
|
+
new Promise(() => {})
|
|
199
|
+
) as unknown as typeof fetch;
|
|
200
|
+
const Wrapper = createWrapper(fetcher);
|
|
201
|
+
|
|
202
|
+
render(
|
|
203
|
+
<Wrapper>
|
|
204
|
+
<SubscriptionManager
|
|
205
|
+
productId="raise-simpli"
|
|
206
|
+
returnUrl="https://app.test.com/settings"
|
|
207
|
+
/>
|
|
208
|
+
</Wrapper>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(screen.getByTestId("subscription-manager")).toBeDefined();
|
|
212
|
+
expect(screen.getByText(/loading/i)).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("shows current plan details when subscribed (plan name, price)", async () => {
|
|
216
|
+
const fetcher = mockFetchRouter({
|
|
217
|
+
"/billing/subscription/current/": mockSubscriptionActive,
|
|
218
|
+
"/billing/products/": mockProduct,
|
|
219
|
+
});
|
|
220
|
+
const Wrapper = createWrapper(fetcher);
|
|
221
|
+
|
|
222
|
+
render(
|
|
223
|
+
<Wrapper>
|
|
224
|
+
<SubscriptionManager
|
|
225
|
+
productId="raise-simpli"
|
|
226
|
+
returnUrl="https://app.test.com/settings"
|
|
227
|
+
/>
|
|
228
|
+
</Wrapper>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await waitFor(() => {
|
|
232
|
+
expect(screen.getByTestId("current-plan")).toBeDefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Check plan name
|
|
236
|
+
expect(screen.getByText("Pro")).toBeDefined();
|
|
237
|
+
|
|
238
|
+
// Check price
|
|
239
|
+
expect(screen.getByText(/\$49/)).toBeDefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("shows subscription status badge (active, trialing, past_due, cancelled)", async () => {
|
|
243
|
+
const fetcher = mockFetchRouter({
|
|
244
|
+
"/billing/subscription/current/": mockSubscriptionActive,
|
|
245
|
+
"/billing/products/": mockProduct,
|
|
246
|
+
});
|
|
247
|
+
const Wrapper = createWrapper(fetcher);
|
|
248
|
+
|
|
249
|
+
render(
|
|
250
|
+
<Wrapper>
|
|
251
|
+
<SubscriptionManager
|
|
252
|
+
productId="raise-simpli"
|
|
253
|
+
returnUrl="https://app.test.com/settings"
|
|
254
|
+
/>
|
|
255
|
+
</Wrapper>
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await waitFor(() => {
|
|
259
|
+
expect(screen.getByTestId("plan-status")).toBeDefined();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Check status badge displays "active"
|
|
263
|
+
expect(screen.getByText(/active/i)).toBeDefined();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("shows trialing badge with trial end date when trialing", async () => {
|
|
267
|
+
const fetcher = mockFetchRouter({
|
|
268
|
+
"/billing/subscription/current/": mockSubscriptionTrialing,
|
|
269
|
+
"/billing/products/": mockProduct,
|
|
270
|
+
});
|
|
271
|
+
const Wrapper = createWrapper(fetcher);
|
|
272
|
+
|
|
273
|
+
render(
|
|
274
|
+
<Wrapper>
|
|
275
|
+
<SubscriptionManager
|
|
276
|
+
productId="raise-simpli"
|
|
277
|
+
returnUrl="https://app.test.com/settings"
|
|
278
|
+
/>
|
|
279
|
+
</Wrapper>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await waitFor(() => {
|
|
283
|
+
expect(screen.getByTestId("plan-status")).toBeDefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Check trialing status
|
|
287
|
+
expect(screen.getByText(/trialing/i)).toBeDefined();
|
|
288
|
+
|
|
289
|
+
// Check trial end date is displayed
|
|
290
|
+
expect(screen.getByText(/2026-01-15/)).toBeDefined();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("shows billing period dates", async () => {
|
|
294
|
+
const fetcher = mockFetchRouter({
|
|
295
|
+
"/billing/subscription/current/": mockSubscriptionActive,
|
|
296
|
+
"/billing/products/": mockProduct,
|
|
297
|
+
});
|
|
298
|
+
const Wrapper = createWrapper(fetcher);
|
|
299
|
+
|
|
300
|
+
render(
|
|
301
|
+
<Wrapper>
|
|
302
|
+
<SubscriptionManager
|
|
303
|
+
productId="raise-simpli"
|
|
304
|
+
returnUrl="https://app.test.com/settings"
|
|
305
|
+
/>
|
|
306
|
+
</Wrapper>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
await waitFor(() => {
|
|
310
|
+
expect(screen.getByTestId("current-plan")).toBeDefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Check billing period dates are displayed
|
|
314
|
+
expect(screen.getByText(/2026-01-01/)).toBeDefined();
|
|
315
|
+
expect(screen.getByText(/2026-02-01/)).toBeDefined();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("shows available offers for upgrade (excludes current plan)", async () => {
|
|
319
|
+
const fetcher = mockFetchRouter({
|
|
320
|
+
"/billing/subscription/current/": mockSubscriptionActive, // Currently on Pro
|
|
321
|
+
"/billing/products/": mockProduct,
|
|
322
|
+
});
|
|
323
|
+
const Wrapper = createWrapper(fetcher);
|
|
324
|
+
|
|
325
|
+
render(
|
|
326
|
+
<Wrapper>
|
|
327
|
+
<SubscriptionManager
|
|
328
|
+
productId="raise-simpli"
|
|
329
|
+
returnUrl="https://app.test.com/settings"
|
|
330
|
+
/>
|
|
331
|
+
</Wrapper>
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
await waitFor(() => {
|
|
335
|
+
expect(screen.getByTestId("available-plans")).toBeDefined();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Should show Starter and Enterprise, but NOT Pro (current plan)
|
|
339
|
+
expect(screen.getByText("Starter")).toBeDefined();
|
|
340
|
+
expect(screen.getByText("Enterprise")).toBeDefined();
|
|
341
|
+
|
|
342
|
+
// Pro should only appear once (in current plan section)
|
|
343
|
+
const proElements = screen.getAllByText("Pro");
|
|
344
|
+
expect(proElements.length).toBe(1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("calls onPlanChange when upgrade option clicked", async () => {
|
|
348
|
+
const onPlanChange = vi.fn();
|
|
349
|
+
const fetcher = mockFetchRouter({
|
|
350
|
+
"/billing/subscription/current/": mockSubscriptionActive, // Currently on Pro
|
|
351
|
+
"/billing/products/": mockProduct,
|
|
352
|
+
});
|
|
353
|
+
const Wrapper = createWrapper(fetcher);
|
|
354
|
+
|
|
355
|
+
render(
|
|
356
|
+
<Wrapper>
|
|
357
|
+
<SubscriptionManager
|
|
358
|
+
productId="raise-simpli"
|
|
359
|
+
returnUrl="https://app.test.com/settings"
|
|
360
|
+
onPlanChange={onPlanChange}
|
|
361
|
+
/>
|
|
362
|
+
</Wrapper>
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
expect(screen.getByTestId("available-plans")).toBeDefined();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Click Enterprise upgrade option
|
|
370
|
+
const enterpriseButton = screen.getByTestId("upgrade-cta-enterprise-monthly");
|
|
371
|
+
await userEvent.click(enterpriseButton);
|
|
372
|
+
|
|
373
|
+
expect(onPlanChange).toHaveBeenCalledWith(
|
|
374
|
+
expect.objectContaining({ slug: "enterprise-monthly" })
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("shows Manage Billing portal button", async () => {
|
|
379
|
+
const fetcher = mockFetchRouter({
|
|
380
|
+
"/billing/subscription/current/": mockSubscriptionActive,
|
|
381
|
+
"/billing/products/": mockProduct,
|
|
382
|
+
});
|
|
383
|
+
const Wrapper = createWrapper(fetcher);
|
|
384
|
+
|
|
385
|
+
render(
|
|
386
|
+
<Wrapper>
|
|
387
|
+
<SubscriptionManager
|
|
388
|
+
productId="raise-simpli"
|
|
389
|
+
returnUrl="https://app.test.com/settings"
|
|
390
|
+
/>
|
|
391
|
+
</Wrapper>
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
await waitFor(() => {
|
|
395
|
+
expect(screen.getByTestId("portal-button")).toBeDefined();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(screen.getByText(/manage billing/i)).toBeDefined();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("shows No active subscription when no subscription exists", async () => {
|
|
402
|
+
const fetcher = mockFetchRouter({
|
|
403
|
+
"/billing/subscription/current/": null, // 404 - no subscription
|
|
404
|
+
"/billing/products/": mockProduct,
|
|
405
|
+
});
|
|
406
|
+
const Wrapper = createWrapper(fetcher);
|
|
407
|
+
|
|
408
|
+
render(
|
|
409
|
+
<Wrapper>
|
|
410
|
+
<SubscriptionManager
|
|
411
|
+
productId="raise-simpli"
|
|
412
|
+
returnUrl="https://app.test.com/settings"
|
|
413
|
+
/>
|
|
414
|
+
</Wrapper>
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
await waitFor(() => {
|
|
418
|
+
expect(screen.getByText(/no active subscription/i)).toBeDefined();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("shows subscribe options when no subscription", async () => {
|
|
423
|
+
const fetcher = mockFetchRouter({
|
|
424
|
+
"/billing/subscription/current/": null, // 404 - no subscription
|
|
425
|
+
"/billing/products/": mockProduct,
|
|
426
|
+
});
|
|
427
|
+
const Wrapper = createWrapper(fetcher);
|
|
428
|
+
|
|
429
|
+
render(
|
|
430
|
+
<Wrapper>
|
|
431
|
+
<SubscriptionManager
|
|
432
|
+
productId="raise-simpli"
|
|
433
|
+
returnUrl="https://app.test.com/settings"
|
|
434
|
+
/>
|
|
435
|
+
</Wrapper>
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
await waitFor(() => {
|
|
439
|
+
expect(screen.getByTestId("available-plans")).toBeDefined();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Should show all available plans
|
|
443
|
+
expect(screen.getByText("Starter")).toBeDefined();
|
|
444
|
+
expect(screen.getByText("Pro")).toBeDefined();
|
|
445
|
+
expect(screen.getByText("Enterprise")).toBeDefined();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("handles subscription fetch error", async () => {
|
|
449
|
+
const fetcher = mockFetchRouter({
|
|
450
|
+
"/billing/subscription/current/": new Error("Network error"),
|
|
451
|
+
"/billing/products/": mockProduct,
|
|
452
|
+
});
|
|
453
|
+
const Wrapper = createWrapper(fetcher);
|
|
454
|
+
|
|
455
|
+
render(
|
|
456
|
+
<Wrapper>
|
|
457
|
+
<SubscriptionManager
|
|
458
|
+
productId="raise-simpli"
|
|
459
|
+
returnUrl="https://app.test.com/settings"
|
|
460
|
+
/>
|
|
461
|
+
</Wrapper>
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
await waitFor(() => {
|
|
465
|
+
expect(screen.getByText(/error/i)).toBeDefined();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(screen.getByText(/network error/i)).toBeDefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("shows past due warning styling", async () => {
|
|
472
|
+
const fetcher = mockFetchRouter({
|
|
473
|
+
"/billing/subscription/current/": mockSubscriptionPastDue,
|
|
474
|
+
"/billing/products/": mockProduct,
|
|
475
|
+
});
|
|
476
|
+
const Wrapper = createWrapper(fetcher);
|
|
477
|
+
|
|
478
|
+
render(
|
|
479
|
+
<Wrapper>
|
|
480
|
+
<SubscriptionManager
|
|
481
|
+
productId="raise-simpli"
|
|
482
|
+
returnUrl="https://app.test.com/settings"
|
|
483
|
+
/>
|
|
484
|
+
</Wrapper>
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
await waitFor(() => {
|
|
488
|
+
expect(screen.getByTestId("plan-status")).toBeDefined();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Check past_due status is displayed
|
|
492
|
+
expect(screen.getByText(/past.due/i)).toBeDefined();
|
|
493
|
+
|
|
494
|
+
// Check for warning styling indicator
|
|
495
|
+
const statusBadge = screen.getByTestId("plan-status");
|
|
496
|
+
expect(statusBadge.className).toMatch(/warning|error|danger|alert/i);
|
|
497
|
+
});
|
|
498
|
+
});
|