import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { FleetView } from '../FleetView'; const useExperimentalMock = vi.fn(() => ({ experimental: true, experimentalReady: true })); vi.mock('@/hooks/useExperimental', () => ({ useExperimental: () => useExperimentalMock(), })); vi.mock('@/context/LicenseContext', () => ({ useLicense: () => ({ isPaid: true, licenseStatus: 'ready' as const }), })); const useAuthMock = vi.fn(() => ({ isAdmin: true as boolean, can: () => true as boolean })); vi.mock('@/context/AuthContext', () => ({ useAuth: () => useAuthMock(), })); vi.mock('@/context/NodeContext', () => ({ useNodes: () => ({ hasCapability: () => false }), })); vi.mock('../FleetView/hooks/useFleetPreferences', () => ({ useFleetPreferences: () => ({ prefs: {}, updatePrefs: vi.fn() }), })); vi.mock('../FleetView/hooks/useFleetUpdateStatus', () => ({ useFleetUpdateStatus: () => ({ updateStatuses: [], localUpdateConfirm: null, setShowUpdateModal: vi.fn(), fetchUpdateStatus: vi.fn(), showUpdateModal: false, checkingUpdates: false, updatingNodeId: null, reconnecting: false, preUpdateStartedAt: null, triggerNodeUpdate: vi.fn(), retryNodeUpdate: vi.fn(), dismissNodeUpdate: vi.fn(), checkUpdates: vi.fn(), confirmLocalUpdate: vi.fn(), cancelLocalUpdate: vi.fn(), }), })); vi.mock('../FleetView/hooks/useFleetOverview', () => ({ useFleetOverview: () => ({ nodes: [], processedNodes: [], allNodes: [], topologyNodes: [], viewMode: 'cards', setViewMode: vi.fn(), searchQuery: '', setSearchQuery: vi.fn(), fleetPalette: {}, labelFilters: {}, setLabelFilters: vi.fn(), clearFilters: vi.fn(), fleetStackLabelMap: {}, updateStatusMap: {}, mastheadStats: { nodeCount: 0, onlineCount: 0, criticalCount: 0, avgCpuNum: 0, worstCpu: 0, totalMemUsed: 0, totalMemTotal: 0, totalContainers: 0, totalContainersAll: 0, }, lastSyncAt: null, loading: false, refreshing: false, fetchOverview: vi.fn(), }), })); vi.mock('../FleetView/hooks/useFleetPolling', () => ({ useFleetPolling: () => {}, })); vi.mock('../FleetView/hooks/useFleetDossierExport', () => ({ useFleetDossierExport: () => ({ exporting: false, exportDossier: vi.fn() }), })); vi.mock('@/hooks/useTopologyPreferences', () => ({ useTopologyPreferences: () => ({ prefs: { mode: 'hub', positions: {} }, setMode: vi.fn(), setPositions: vi.fn() }), })); vi.mock('../nodes/useNodeActions', () => ({ useNodeActions: () => ({ openEdit: vi.fn(), openDelete: vi.fn(), NodeActionModals: null }), })); vi.mock('../fleet/FleetMasthead', () => ({ FleetMasthead: () =>
})); vi.mock('../FleetView/OverviewTab', () => ({ OverviewTab: () =>
})); vi.mock('../FleetView/ReconnectingOverlay', () => ({ ReconnectingOverlay: () => null })); vi.mock('../FleetView/NodeUpdatesSheet', () => ({ NodeUpdatesSheet: () => null })); vi.mock('../FleetView/LocalUpdateConfirmDialog', () => ({ LocalUpdateConfirmDialog: () => null })); vi.mock('../FleetSnapshots', () => ({ default: () => null })); vi.mock('../fleet/FleetConfiguration', () => ({ FleetConfiguration: () => null })); vi.mock('../fleet/RoutingTab', () => ({ RoutingTab: () =>
})); vi.mock('../fleet/FederationTab', () => ({ FederationTab: () =>
})); vi.mock('../blueprints/DeploymentsTab', () => ({ DeploymentsTab: () =>
})); vi.mock('../fleet/FleetActions/FleetActionsTab', () => ({ FleetActionsTab: () =>
})); vi.mock('../fleet/secrets/SecretsTab', () => ({ SecretsTab: () =>
})); vi.mock('../fleet/DependencyMapTab', () => ({ DependencyMapTab: () => null })); vi.mock('../fleet/ContainerLabelsTab', () => ({ ContainerLabelsTab: () => null })); vi.mock('../PaidGate', () => ({ PaidGate: ({ children }: { children: React.ReactNode }) => <>{children} })); describe('FleetView tab discovery and deep-link fallback', () => { beforeEach(() => { useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true }); useAuthMock.mockReturnValue({ isAdmin: true, can: () => true }); }); it('shows Routing when experimental is on; Secrets always visible for Admin', () => { render(); expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy(); expect(screen.getByRole('tab', { name: /secrets/i })).toBeTruthy(); }); it('hides Routing when experimental off; Secrets stays visible for Admin', () => { useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true }); render(); expect(screen.queryByRole('tab', { name: /routing/i })).toBeNull(); expect(screen.getByRole('tab', { name: /secrets/i })).toBeTruthy(); expect(screen.getByRole('tab', { name: /deployments/i })).toBeTruthy(); expect(screen.getByRole('tab', { name: /federation/i })).toBeTruthy(); expect(screen.getByRole('tab', { name: /actions/i })).toBeTruthy(); }); it('delayed experimental true preserves controlled routing without Overview callback', async () => { useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: false }); const onTab = vi.fn(); const { rerender } = render( , ); expect(screen.queryByRole('tab', { name: /routing/i })).toBeNull(); expect(onTab).not.toHaveBeenCalled(); useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true }); rerender( , ); await waitFor(() => expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy()); expect(onTab).not.toHaveBeenCalled(); }); it('resolved experimental false falls back controlled routing to overview once', async () => { useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: false }); const onTab = vi.fn(); const { rerender } = render( , ); expect(onTab).not.toHaveBeenCalled(); useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true }); rerender( , ); await waitFor(() => expect(onTab).toHaveBeenCalledWith('overview')); expect(onTab).toHaveBeenCalledTimes(1); }); it('does not rewrite a Secrets deep link for Admin when experimental is off', () => { useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true }); const onTab = vi.fn(); render( , ); // No fallback rewrite; Secrets is always available to Admin expect(onTab).not.toHaveBeenCalled(); }); it('hides Secrets tab for non-admin', () => { useAuthMock.mockReturnValue({ isAdmin: false, can: () => false }); render(); expect(screen.queryByRole('tab', { name: /secrets/i })).toBeNull(); // Routing still visible (gated on experimental + paid, not admin) expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy(); // Unrelated tabs still visible expect(screen.getByRole('tab', { name: /deployments/i })).toBeTruthy(); }); });