11import { describe , it , expect , beforeEach , vi } from "vitest" ;
22import { render , screen , waitFor , fireEvent } from "@testing-library/react" ;
33
4+ // Mock the api module — CloudSettings now uses typed API helpers, not raw fetch
5+ vi . mock ( "../../../lib/api" , ( ) => ( {
6+ getCloudStatus : vi . fn ( ) ,
7+ getLoginUrl : vi . fn ( ) ,
8+ getProfile : vi . fn ( ) ,
9+ getBalance : vi . fn ( ) ,
10+ logout : vi . fn ( ) ,
11+ } ) ) ;
12+
13+ import {
14+ getCloudStatus ,
15+ getProfile ,
16+ getBalance ,
17+ logout as apiLogout ,
18+ } from "../../../lib/api" ;
19+
420const SAMPLE_PROFILE = {
521 user_id : "u-1" ,
622 email : "test@example.com" ,
7- github_handle : "testuser" ,
23+ display_name : "testuser" ,
24+ plan : "pro" ,
25+ credits_balance : 42 ,
26+ } ;
27+
28+ const SAMPLE_BALANCE = {
829 plan : "pro" ,
930 credits_balance : 42 ,
10- trial_counts : { logo : 2 , name : 1 , northstar : 0 , scrape : 0 , crawl : 1 , vision : 2 , search : 0 } ,
31+ subscription_url : "https://example.com/subscribe" ,
32+ topup_url : "https://example.com/topup" ,
1133} ;
1234
1335beforeEach ( ( ) => {
1436 vi . restoreAllMocks ( ) ;
1537} ) ;
1638
1739describe ( "CloudSettings" , ( ) => {
18- it ( "renders 'Sign in' when unauthenticated (fetch returns 401)" , async ( ) => {
19- vi . stubGlobal ( "fetch" , vi . fn ( ( ) => Promise . resolve ( { ok : false , json : ( ) => Promise . resolve ( null ) } ) ) ) ;
40+ it ( "renders 'Sign in' when unauthenticated" , async ( ) => {
41+ vi . mocked ( getCloudStatus ) . mockResolvedValue ( {
42+ cloud_available : true ,
43+ authenticated : false ,
44+ plan : null ,
45+ credits_balance : null ,
46+ cloud_generation_enabled : false ,
47+ } ) ;
2048 const { CloudSettings } = await import ( "../CloudSettings" ) ;
2149 render ( < CloudSettings /> ) ;
2250 await waitFor ( ( ) => expect ( screen . getByTestId ( "cloud-unauthenticated" ) ) . toBeInTheDocument ( ) ) ;
2351 expect ( screen . getByTestId ( "sign-in-button" ) ) . toBeInTheDocument ( ) ;
2452 } ) ;
2553
54+ it ( "renders unavailable when cloud not configured" , async ( ) => {
55+ vi . mocked ( getCloudStatus ) . mockResolvedValue ( {
56+ cloud_available : false ,
57+ authenticated : false ,
58+ plan : null ,
59+ credits_balance : null ,
60+ cloud_generation_enabled : false ,
61+ } ) ;
62+ const { CloudSettings } = await import ( "../CloudSettings" ) ;
63+ render ( < CloudSettings /> ) ;
64+ await waitFor ( ( ) => expect ( screen . getByTestId ( "cloud-unavailable" ) ) . toBeInTheDocument ( ) ) ;
65+ } ) ;
66+
2667 it ( "renders email and plan when authenticated" , async ( ) => {
27- vi . stubGlobal ( "fetch" , vi . fn ( ( ) => Promise . resolve ( {
28- ok : true ,
29- json : ( ) => Promise . resolve ( SAMPLE_PROFILE ) ,
30- } ) ) ) ;
68+ vi . mocked ( getCloudStatus ) . mockResolvedValue ( {
69+ cloud_available : true ,
70+ authenticated : true ,
71+ plan : "pro" ,
72+ credits_balance : 42 ,
73+ cloud_generation_enabled : true ,
74+ } ) ;
75+ vi . mocked ( getProfile ) . mockResolvedValue ( SAMPLE_PROFILE ) ;
76+ vi . mocked ( getBalance ) . mockResolvedValue ( SAMPLE_BALANCE ) ;
3177 const { CloudSettings } = await import ( "../CloudSettings" ) ;
3278 render ( < CloudSettings /> ) ;
3379 await waitFor ( ( ) => expect ( screen . getByTestId ( "cloud-authenticated" ) ) . toBeInTheDocument ( ) ) ;
@@ -36,41 +82,70 @@ describe("CloudSettings", () => {
3682 } ) ;
3783
3884 it ( "renders credit balance" , async ( ) => {
39- vi . stubGlobal ( "fetch" , vi . fn ( ( ) => Promise . resolve ( {
40- ok : true ,
41- json : ( ) => Promise . resolve ( SAMPLE_PROFILE ) ,
42- } ) ) ) ;
85+ vi . mocked ( getCloudStatus ) . mockResolvedValue ( {
86+ cloud_available : true ,
87+ authenticated : true ,
88+ plan : "pro" ,
89+ credits_balance : 42 ,
90+ cloud_generation_enabled : true ,
91+ } ) ;
92+ vi . mocked ( getProfile ) . mockResolvedValue ( SAMPLE_PROFILE ) ;
93+ vi . mocked ( getBalance ) . mockResolvedValue ( SAMPLE_BALANCE ) ;
4394 const { CloudSettings } = await import ( "../CloudSettings" ) ;
4495 render ( < CloudSettings /> ) ;
4596 await waitFor ( ( ) => expect ( screen . getByTestId ( "credit-balance" ) ) . toBeInTheDocument ( ) ) ;
4697 expect ( screen . getByTestId ( "credit-balance" ) ) . toHaveTextContent ( "42" ) ;
4798 } ) ;
4899
49100 it ( "renders trial badges for all features" , async ( ) => {
50- vi . stubGlobal ( "fetch" , vi . fn ( ( ) => Promise . resolve ( {
51- ok : true ,
52- json : ( ) => Promise . resolve ( SAMPLE_PROFILE ) ,
53- } ) ) ) ;
101+ vi . mocked ( getCloudStatus ) . mockResolvedValue ( {
102+ cloud_available : true ,
103+ authenticated : true ,
104+ plan : "pro" ,
105+ credits_balance : 42 ,
106+ cloud_generation_enabled : true ,
107+ } ) ;
108+ vi . mocked ( getProfile ) . mockResolvedValue ( SAMPLE_PROFILE ) ;
109+ vi . mocked ( getBalance ) . mockResolvedValue ( SAMPLE_BALANCE ) ;
54110 const { CloudSettings } = await import ( "../CloudSettings" ) ;
55111 render ( < CloudSettings /> ) ;
56112 await waitFor ( ( ) => expect ( screen . getByTestId ( "trial-logo" ) ) . toBeInTheDocument ( ) ) ;
57- // logo: 2 used → 0 remaining
58- expect ( screen . getByTestId ( "trial-count-logo" ) ) . toHaveTextContent ( "0 remaining" ) ;
59- // name: 1 used → 1 remaining
60- expect ( screen . getByTestId ( "trial-count-name" ) ) . toHaveTextContent ( "1 remaining" ) ;
113+ expect ( screen . getByTestId ( "trial-count-logo" ) ) . toHaveTextContent ( "2 remaining" ) ;
61114 } ) ;
62115
63- it ( "sign out button clears profile" , async ( ) => {
64- const mockFetch = vi . fn ( )
65- . mockResolvedValueOnce ( { ok : true , json : ( ) => Promise . resolve ( SAMPLE_PROFILE ) } )
66- . mockResolvedValueOnce ( { ok : true , json : ( ) => Promise . resolve ( null ) } ) ;
67- vi . stubGlobal ( "fetch" , mockFetch ) ;
116+ it ( "sign out button returns to unauthenticated" , async ( ) => {
117+ vi . mocked ( getCloudStatus ) . mockResolvedValue ( {
118+ cloud_available : true ,
119+ authenticated : true ,
120+ plan : "pro" ,
121+ credits_balance : 42 ,
122+ cloud_generation_enabled : true ,
123+ } ) ;
124+ vi . mocked ( getProfile ) . mockResolvedValue ( SAMPLE_PROFILE ) ;
125+ vi . mocked ( getBalance ) . mockResolvedValue ( SAMPLE_BALANCE ) ;
126+ vi . mocked ( apiLogout ) . mockResolvedValue ( { success : true } ) ;
68127 const { CloudSettings } = await import ( "../CloudSettings" ) ;
69128 render ( < CloudSettings /> ) ;
70129 await waitFor ( ( ) => expect ( screen . getByTestId ( "logout-button" ) ) . toBeInTheDocument ( ) ) ;
71130 fireEvent . click ( screen . getByTestId ( "logout-button" ) ) ;
72131 await waitFor ( ( ) => expect ( screen . getByTestId ( "cloud-unauthenticated" ) ) . toBeInTheDocument ( ) ) ;
73132 } ) ;
133+
134+ it ( "shows topup link from balance response" , async ( ) => {
135+ vi . mocked ( getCloudStatus ) . mockResolvedValue ( {
136+ cloud_available : true ,
137+ authenticated : true ,
138+ plan : "pro" ,
139+ credits_balance : 42 ,
140+ cloud_generation_enabled : true ,
141+ } ) ;
142+ vi . mocked ( getProfile ) . mockResolvedValue ( SAMPLE_PROFILE ) ;
143+ vi . mocked ( getBalance ) . mockResolvedValue ( SAMPLE_BALANCE ) ;
144+ const { CloudSettings } = await import ( "../CloudSettings" ) ;
145+ render ( < CloudSettings /> ) ;
146+ await waitFor ( ( ) => expect ( screen . getByTestId ( "topup-link" ) ) . toBeInTheDocument ( ) ) ;
147+ expect ( screen . getByTestId ( "topup-link" ) ) . toHaveAttribute ( "href" , "https://example.com/topup" ) ;
148+ } ) ;
74149} ) ;
75150
76151describe ( "TrialBadge" , ( ) => {
0 commit comments