@@ -174,6 +174,14 @@ vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-panel', () =
174174 ) ,
175175} ) )
176176
177+ vi . mock ( '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' , ( ) => ( {
178+ SettingsEmptyState : ( { children, tone } : { children : ReactNode ; tone ?: 'muted' | 'error' } ) => (
179+ < div data-testid = 'settings-empty-state' data-tone = { tone ?? 'muted' } >
180+ { children }
181+ </ div >
182+ ) ,
183+ } ) )
184+
177185vi . mock (
178186 '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' ,
179187 ( ) => ( {
@@ -269,13 +277,7 @@ describe('Billing payer scope', () => {
269277
270278 it ( 'uses the target organization DTO for annual, canceled, credit, cap, and link state' , async ( ) => {
271279 await act ( async ( ) => {
272- root . render (
273- < Billing
274- scope = 'organization'
275- organizationId = 'org-target'
276- governingWorkspaceName = 'Production'
277- />
278- )
280+ root . render ( < Billing scope = 'organization' organizationId = 'org-target' /> )
279281 } )
280282
281283 expect ( mockUseSubscriptionData ) . toHaveBeenCalledWith (
@@ -290,9 +292,7 @@ describe('Billing payer scope', () => {
290292 container . querySelector ( 'a[href="/workspace/organization-workspace/upgrade"]' ) ?. textContent
291293 ) . toBe ( 'Explore organization plans' )
292294 expect ( container . textContent ) . toContain ( 'Organization Max for Teams plan' )
293- expect ( container . textContent ) . toContain (
294- 'Target organization’s subscription governs Production.'
295- )
295+ expect ( container . querySelector ( 'main > p' ) ) . toBeNull ( )
296296 expect ( container . textContent ) . toContain ( 'billed annually' )
297297 expect ( container . textContent ) . toContain ( 'Access until' )
298298 expect ( container . textContent ) . toContain ( 'Subscription canceled' )
@@ -316,19 +316,45 @@ describe('Billing payer scope', () => {
316316
317317 it ( 'uses a guaranteed personal payer workspace for account upgrades' , async ( ) => {
318318 await act ( async ( ) => {
319- root . render ( < Billing scope = 'account' governingWorkspaceName = 'Personal workspace' /> )
319+ root . render ( < Billing scope = 'account' /> )
320320 } )
321321
322322 expect (
323323 container . querySelector ( 'a[href="/workspace/personal-workspace/upgrade"]' ) ?. textContent
324324 ) . toBe ( 'Explore personal plans' )
325325 expect ( container . textContent ) . toContain ( 'Personal Pro plan' )
326- expect ( container . textContent ) . toContain (
327- 'Your personal subscription governs Personal workspace.'
328- )
329326 } )
330327
331- it ( 'does not show a governing subscription description for a free personal workspace' , async ( ) => {
328+ it ( 'does not override the route-owned header while billing transitions from loading to success' , async ( ) => {
329+ mockPersonalQuery . current = {
330+ data : undefined ,
331+ error : null ,
332+ isLoading : true ,
333+ refetch : vi . fn ( ) ,
334+ }
335+
336+ await act ( async ( ) => {
337+ root . render ( < Billing scope = 'account' /> )
338+ } )
339+
340+ expect ( container . innerHTML ) . toBe ( '' )
341+
342+ mockPersonalQuery . current = {
343+ data : { success : true , context : 'user' , data : PERSONAL_DATA } ,
344+ error : null ,
345+ isLoading : false ,
346+ refetch : vi . fn ( ) ,
347+ }
348+
349+ await act ( async ( ) => {
350+ root . render ( < Billing scope = 'account' /> )
351+ } )
352+
353+ expect ( container . textContent ) . toContain ( 'Personal Pro plan' )
354+ expect ( container . querySelector ( 'main > p' ) ) . toBeNull ( )
355+ } )
356+
357+ it ( 'does not add a dynamic header description for a free personal workspace' , async ( ) => {
332358 mockPersonalQuery . current = {
333359 data : {
334360 success : true ,
@@ -340,7 +366,7 @@ describe('Billing payer scope', () => {
340366 }
341367
342368 await act ( async ( ) => {
343- root . render ( < Billing scope = 'account' governingWorkspaceName = 'Free workspace' /> )
369+ root . render ( < Billing scope = 'account' /> )
344370 } )
345371
346372 expect ( container . textContent ) . toContain ( 'Personal Free plan' )
@@ -368,13 +394,7 @@ describe('Billing payer scope', () => {
368394 }
369395
370396 await act ( async ( ) => {
371- root . render (
372- < Billing
373- scope = 'organization'
374- organizationId = 'org-target'
375- governingWorkspaceName = 'Free organization workspace'
376- />
377- )
397+ root . render ( < Billing scope = 'organization' organizationId = 'org-target' /> )
378398 } )
379399
380400 expect ( container . textContent ) . toContain ( 'Organization Free plan' )
@@ -398,13 +418,7 @@ describe('Billing payer scope', () => {
398418 }
399419
400420 await act ( async ( ) => {
401- root . render (
402- < Billing
403- scope = 'organization'
404- organizationId = 'org-target'
405- governingWorkspaceName = 'Lapsed organization workspace'
406- />
407- )
421+ root . render ( < Billing scope = 'organization' organizationId = 'org-target' /> )
408422 } )
409423
410424 expect ( container . textContent ) . toContain ( 'Organization Max for Teams plan ended' )
@@ -415,4 +429,54 @@ describe('Billing payer scope', () => {
415429 container . querySelector ( 'a[href="/workspace/organization-workspace/upgrade"]' ) ?. textContent
416430 ) . toBe ( 'Explore organization plans' )
417431 } )
432+
433+ it ( 'renders the canonical error state when the active billing query fails' , async ( ) => {
434+ mockPersonalQuery . current = {
435+ data : undefined ,
436+ error : new Error ( 'Billing temporarily unavailable' ) ,
437+ isLoading : false ,
438+ refetch : vi . fn ( ) ,
439+ }
440+
441+ await act ( async ( ) => {
442+ root . render ( < Billing scope = 'account' /> )
443+ } )
444+
445+ const errorState = container . querySelector ( '[data-testid="settings-empty-state"]' )
446+ expect ( errorState ) . toHaveAttribute ( 'data-tone' , 'error' )
447+ expect ( errorState ?. textContent ) . toBe ( 'Billing temporarily unavailable' )
448+ } )
449+
450+ it ( 'keeps cached billing content visible when a background refresh fails' , async ( ) => {
451+ mockPersonalQuery . current = {
452+ data : { success : true , context : 'user' , data : PERSONAL_DATA } ,
453+ error : new Error ( 'Background refresh failed' ) ,
454+ isLoading : false ,
455+ refetch : vi . fn ( ) ,
456+ }
457+
458+ await act ( async ( ) => {
459+ root . render ( < Billing scope = 'account' /> )
460+ } )
461+
462+ expect ( container . textContent ) . toContain ( 'Personal Pro plan' )
463+ expect ( container . querySelector ( '[data-testid="settings-empty-state"]' ) ) . toBeNull ( )
464+ } )
465+
466+ it ( 'renders the canonical fallback error when billing completes without data' , async ( ) => {
467+ mockOrganizationQuery . current = {
468+ data : undefined ,
469+ error : null ,
470+ isLoading : false ,
471+ refetch : vi . fn ( ) ,
472+ }
473+
474+ await act ( async ( ) => {
475+ root . render ( < Billing scope = 'organization' organizationId = 'org-target' /> )
476+ } )
477+
478+ const errorState = container . querySelector ( '[data-testid="settings-empty-state"]' )
479+ expect ( errorState ) . toHaveAttribute ( 'data-tone' , 'error' )
480+ expect ( errorState ?. textContent ) . toBe ( 'Failed to load billing information' )
481+ } )
418482} )
0 commit comments