diff --git a/ui/tests/unit/components/account/flow-viz/index.test.tsx b/ui/tests/unit/components/account/flow-viz/index.test.tsx new file mode 100644 index 00000000..3db3da49 --- /dev/null +++ b/ui/tests/unit/components/account/flow-viz/index.test.tsx @@ -0,0 +1,161 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen, userEvent } from '@tests/setup/test-utils'; +import { AccountFlowViz } from '@/components/account-flow-viz'; +import type { ProviderData } from '@/components/account/flow-viz/types'; + +vi.mock(import('react-i18next'), async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + useTranslation: () => ({ + t: (key: string, options?: { count?: number }) => { + const translations: Record = { + 'flowViz.showDetails': 'Show Details', + 'flowViz.hideDetails': 'Hide Details', + 'flowViz.backToProviders': 'Back to providers', + 'flowViz.resetLayout': 'Reset layout', + }; + + if (key === 'flowViz.showPausedAccounts') { + return `Show Paused (${options?.count ?? 0})`; + } + + if (key === 'flowViz.hidePausedAccounts') { + return `Hide Paused (${options?.count ?? 0})`; + } + + return translations[key] ?? key; + }, + }), + }; +}); + +vi.mock('@/components/account/flow-viz/account-card', () => ({ + AccountCard: ({ + account, + originalIndex, + zone, + }: { + account: { email: string; paused?: boolean }; + originalIndex: number; + zone: string; + }) => ( +
+ {account.email} + {account.paused ? ' (paused)' : ' (active)'} +
+ ), +})); + +vi.mock('@/components/account/flow-viz/provider-card', () => ({ + ProviderCard: ({ providerData }: { providerData: ProviderData }) => ( +
{providerData.accounts.length} visible accounts
+ ), +})); + +vi.mock('@/components/account/flow-viz/connection-timeline', () => ({ + ConnectionTimeline: ({ events }: { events: Array<{ id: string }> }) => ( +
{events.length} timeline events
+ ), +})); + +vi.mock('@/components/account/flow-viz/flow-paths', () => ({ + FlowPaths: ({ accounts }: { accounts: Array<{ id: string }> }) => ( +
paths:{accounts.map((account) => account.id).join(',')}
+ ), +})); + +vi.mock('@/components/account/flow-viz/path-utils', () => ({ + calculateBezierPaths: () => [], +})); + +const providerData: ProviderData = { + provider: 'codex', + displayName: 'OpenAI Codex', + totalRequests: 7, + accounts: [ + { + id: 'paused-account', + email: 'paused@company.com', + provider: 'codex', + successCount: 4, + failureCount: 0, + color: '#f59e0b', + paused: true, + }, + { + id: 'active-account', + email: 'active@company.com', + provider: 'codex', + successCount: 2, + failureCount: 1, + color: '#10a37f', + }, + ], +}; + +describe('AccountFlowViz paused account visibility', () => { + beforeEach(() => { + vi.mocked(window.localStorage.getItem).mockReturnValue(null); + vi.stubGlobal( + 'requestAnimationFrame', + vi.fn(() => 0) + ); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('hides and restores paused accounts from the provider detail visualization', async () => { + render(); + + expect(screen.getByRole('button', { name: 'Show Details' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Hide Paused (1)' })).toBeInTheDocument(); + expect(screen.getByText('active@company.com (active)')).toBeInTheDocument(); + expect(screen.getByText('paused@company.com (paused)')).toBeInTheDocument(); + expect(screen.getByText('2 visible accounts')).toBeInTheDocument(); + expect(screen.getByText('7 timeline events')).toBeInTheDocument(); + expect(screen.getByText('paths:paused-account,active-account')).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'Hide Paused (1)' })); + + expect(screen.getByRole('button', { name: 'Show Paused (1)' })).toHaveAttribute( + 'aria-pressed', + 'true' + ); + expect(screen.getByText('active@company.com (active)')).toBeInTheDocument(); + expect(screen.queryByText('paused@company.com (paused)')).not.toBeInTheDocument(); + expect(screen.getByText('1 visible accounts')).toBeInTheDocument(); + expect(screen.getByText('3 timeline events')).toBeInTheDocument(); + expect(screen.getByText('paths:active-account')).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'Show Paused (1)' })); + + expect(screen.getByRole('button', { name: 'Hide Paused (1)' })).toHaveAttribute( + 'aria-pressed', + 'false' + ); + expect(screen.getByText('paused@company.com (paused)')).toBeInTheDocument(); + expect(screen.getByText('2 visible accounts')).toBeInTheDocument(); + }); + + it('ignores hidden paused-account drag offsets when deciding whether reset layout should stay visible', async () => { + vi.mocked(window.localStorage.getItem).mockImplementation((key: string) => + key === 'ccs-flow-positions-codex' + ? JSON.stringify({ + 'paused-account': { x: 0, y: 160 }, + }) + : null + ); + + render(); + + expect(screen.getByRole('button', { name: 'Reset layout' })).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: 'Hide Paused (1)' })); + + expect(screen.queryByRole('button', { name: 'Reset layout' })).not.toBeInTheDocument(); + }); +});