diff --git a/frontend/packages/console-app/src/components/nodes/NodeTerminal.tsx b/frontend/packages/console-app/src/components/nodes/NodeTerminal.tsx index 75f7e2c4512..29b5dc338c8 100644 --- a/frontend/packages/console-app/src/components/nodes/NodeTerminal.tsx +++ b/frontend/packages/console-app/src/components/nodes/NodeTerminal.tsx @@ -244,16 +244,22 @@ const NodeTerminal: FC = ({ obj: node }) => { createDebugPod(); window.addEventListener('beforeunload', closeTab); return () => { - deleteNamespace(namespace.metadata.name); + if (namespace) { + deleteNamespace(namespace.metadata.name); + } window.removeEventListener('beforeunload', closeTab); }; }, [nodeName, isWindows]); - return errorMessage ? ( - - ) : ( - - ); + if (errorMessage) { + return ; + } + + if (!podName) { + return ; + } + + return ; }; export default NodeTerminal; diff --git a/frontend/packages/console-app/src/components/nodes/__tests__/NodeTerminal.spec.tsx b/frontend/packages/console-app/src/components/nodes/__tests__/NodeTerminal.spec.tsx new file mode 100644 index 00000000000..4760e2f2927 --- /dev/null +++ b/frontend/packages/console-app/src/components/nodes/__tests__/NodeTerminal.spec.tsx @@ -0,0 +1,152 @@ +import { render, screen, act } from '@testing-library/react'; +import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; +import type { NodeKind, PodKind } from '@console/internal/module/k8s'; +import { k8sCreate, k8sGet, k8sKillByName } from '@console/internal/module/k8s'; +import NodeTerminal from '../NodeTerminal'; + +jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ + useK8sWatchResource: jest.fn(), +})); + +jest.mock('@console/internal/components/pod', () => ({ + PodConnectLoader: jest.fn(() => 'PodConnectLoader'), +})); + +jest.mock('@console/internal/module/k8s', () => ({ + k8sCreate: jest.fn(), + k8sGet: jest.fn(), + k8sKillByName: jest.fn(), +})); + +const mockNode = { + apiVersion: 'v1', + kind: 'Node', + metadata: { name: 'test-node', uid: 'test-uid' }, + status: { nodeInfo: { operatingSystem: 'linux' } }, +} as NodeKind; + +const mockNamespace = { metadata: { name: 'openshift-debug-abc' } }; + +const mockPod = { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'test-node-debug', namespace: 'openshift-debug-abc' }, +}; + +const setupPodCreation = () => { + (k8sCreate as jest.Mock).mockResolvedValueOnce(mockNamespace).mockResolvedValueOnce(mockPod); + (k8sGet as jest.Mock).mockRejectedValue(new Error('not found')); + (k8sKillByName as jest.Mock).mockResolvedValue({}); +}; + +const renderAndCreatePod = async () => { + jest.useFakeTimers(); + await act(async () => { + render(); + }); + await act(async () => { + jest.advanceTimersByTime(1100); + }); + jest.useRealTimers(); +}; + +describe('NodeTerminal', () => { + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(); + jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + jest.useRealTimers(); + }); + + it('should show loading spinner while debug pod is being created', () => { + (k8sCreate as jest.Mock).mockReturnValue(new Promise(() => {})); + (k8sGet as jest.Mock).mockReturnValue(new Promise(() => {})); + (useK8sWatchResource as jest.Mock).mockReturnValue([undefined, true, undefined]); + + render(); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect(screen.queryByText('Debug pod not found or was deleted.')).not.toBeInTheDocument(); + }); + + it('should show error when watch returns a load error', async () => { + setupPodCreation(); + (useK8sWatchResource as jest.Mock).mockImplementation((resource) => + resource ? [{}, true, new Error('Connection refused')] : [undefined, true, undefined], + ); + + await renderAndCreatePod(); + + expect(screen.getByText('Connection refused')).toBeVisible(); + }); + + it('should show loading when watch has not loaded yet', async () => { + setupPodCreation(); + (useK8sWatchResource as jest.Mock).mockImplementation((resource) => + resource ? [{}, false, undefined] : [undefined, true, undefined], + ); + + await renderAndCreatePod(); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + + it('should show not found error when pod is loaded but missing', async () => { + setupPodCreation(); + (useK8sWatchResource as jest.Mock).mockImplementation((resource) => + resource ? [undefined, true, undefined] : [undefined, true, undefined], + ); + + await renderAndCreatePod(); + + expect(screen.getByText('Debug pod not found or was deleted.')).toBeVisible(); + }); + + it('should show error with message when pod phase is Failed', async () => { + const failedPod = ({ + ...mockPod, + status: { phase: 'Failed', message: 'ImagePullBackOff' }, + } as unknown) as PodKind; + + setupPodCreation(); + (useK8sWatchResource as jest.Mock).mockImplementation((resource) => + resource ? [failedPod, true, undefined] : [undefined, true, undefined], + ); + + await renderAndCreatePod(); + + expect(screen.getByText(/The debug pod failed.*ImagePullBackOff/)).toBeVisible(); + }); + + it('should render terminal when pod is Running', async () => { + const runningPod = ({ + ...mockPod, + status: { phase: 'Running' }, + } as unknown) as PodKind; + + setupPodCreation(); + (useK8sWatchResource as jest.Mock).mockImplementation((resource) => + resource ? [runningPod, true, undefined] : [undefined, true, undefined], + ); + + await renderAndCreatePod(); + + expect(screen.getByText('PodConnectLoader')).toBeInTheDocument(); + }); + + it('should show error when pod creation fails', async () => { + (k8sCreate as jest.Mock).mockRejectedValue(new Error('Forbidden')); + (k8sGet as jest.Mock).mockRejectedValue(new Error('not found')); + (k8sKillByName as jest.Mock).mockResolvedValue({}); + (useK8sWatchResource as jest.Mock).mockReturnValue([undefined, true, undefined]); + + await act(async () => { + render(); + }); + + expect(screen.getByText('Forbidden')).toBeVisible(); + }); +});