@@ -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