From feae93029382de67e3b88849188a08fc17a9abd8 Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:17:40 +1200 Subject: [PATCH] fix(node): allow switching tabs [r8s-546] (#1161) --- app/react/components/Widget/WidgetTabs.tsx | 6 +- .../cluster/NodeView/NodeView.test.tsx | 119 ++++++++++++++++++ .../kubernetes/cluster/NodeView/NodeView.tsx | 4 +- 3 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 app/react/kubernetes/cluster/NodeView/NodeView.test.tsx diff --git a/app/react/components/Widget/WidgetTabs.tsx b/app/react/components/Widget/WidgetTabs.tsx index a01fb1753..3ff0a08b5 100644 --- a/app/react/components/Widget/WidgetTabs.tsx +++ b/app/react/components/Widget/WidgetTabs.tsx @@ -74,8 +74,8 @@ export function findSelectedTabIndex( } export function useCurrentTabIndex(tabs: Tab[]) { - const prarms = useCurrentStateAndParams(); - const currentTabIndex = findSelectedTabIndex(prarms, tabs); + const params = useCurrentStateAndParams(); + const currentTabIndex = findSelectedTabIndex(params, tabs); - return [currentTabIndex]; + return currentTabIndex; } diff --git a/app/react/kubernetes/cluster/NodeView/NodeView.test.tsx b/app/react/kubernetes/cluster/NodeView/NodeView.test.tsx new file mode 100644 index 000000000..a3f3ad880 --- /dev/null +++ b/app/react/kubernetes/cluster/NodeView/NodeView.test.tsx @@ -0,0 +1,119 @@ +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; +import { ReactNode } from 'react'; + +import { withTestRouter } from '@/react/test-utils/withRouter'; +import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { withUserProvider } from '@/react/test-utils/withUserProvider'; +import { UserViewModel } from '@/portainer/models/user'; + +import { NodeView } from './NodeView'; + +let mockParams: { endpointId: number; nodeName: string; tab?: string } = { + endpointId: 1, + nodeName: 'test-node', +}; + +// Mock Link component to avoid ui-router relative state resolution in tests +vi.mock('@@/Link', () => ({ + Link: ({ + children, + 'data-cy': dataCy, + params, + ...props + }: { + children: ReactNode; + 'data-cy'?: string; + params?: { tab?: string }; + }) => ( + { + if (params?.tab) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + mockParams = { ...mockParams, tab: params.tab }; + } + }} + {...props} + > + {children} + + ), +})); + +vi.mock('@uirouter/react', async (importOriginal: () => Promise) => ({ + ...(await importOriginal()), + useCurrentStateAndParams: vi.fn(() => ({ params: mockParams })), +})); + +vi.mock('./NodeApplicationsDatatable/NodeApplicationsDatatable', () => ({ + NodeApplicationsDatatable: () =>
, +})); + +vi.mock('./NodeDetails/NodeDetails', () => ({ + NodeDetails: () =>
Node details content
, +})); + +vi.mock('../../components/EventsDatatable/ResourceEventsDatatable', () => ({ + ResourceEventsDatatable: () => ( +
Events content
+ ), +})); + +vi.mock('./NodeYamlInspector', () => ({ + NodeYamlInspector: () =>
YAML content
, +})); + +vi.mock('@/react/hooks/useEnvironmentId', () => ({ + useEnvironmentId: () => 1, +})); + +vi.mock('../queries/useNodeQuery', () => ({ + useNodeQuery: () => ({ isInitialLoading: false, data: 'uid-123' }), +})); + +vi.mock('../../queries/useEvents', () => ({ + useEventWarningsCount: () => 0, +})); + +function getWrapped() { + const user = new UserViewModel({ Username: 'admin' }); + const routerConfig = [{ name: 'root', url: '/' }]; + return withTestQueryProvider( + withUserProvider( + withTestRouter(NodeView, { route: 'root', stateConfig: routerConfig }), + user + ) + ); +} + +describe('NodeView tabs', () => { + it('switches tabs when user clicks different tab', async () => { + const Wrapped = getWrapped(); + const utils = render(); + + // initial tab is first: Node details + expect(screen.queryByTestId('node-details')).toBeVisible(); + expect(screen.queryByTestId('events-table')).toBeNull(); + expect(screen.queryByTestId('yaml-view')).toBeNull(); + + // click Events tab and rerender + screen.getByTestId('tab-1').click(); + utils.rerender(); + expect(screen.queryByTestId('node-details')).toBeNull(); + expect(screen.queryByTestId('events-table')).toBeVisible(); + + // click YAML tab and rerender + screen.getByTestId('tab-2').click(); + utils.rerender(); + expect(screen.queryByTestId('events-table')).toBeNull(); + expect(screen.queryByTestId('yaml-view')).toBeVisible(); + + // back to Node tab (namespace) and rerender + screen.getByTestId('tab-0').click(); + utils.rerender(); + expect(screen.queryByTestId('yaml-view')).toBeNull(); + expect(screen.queryByTestId('node-details')).toBeVisible(); + }); +}); diff --git a/app/react/kubernetes/cluster/NodeView/NodeView.tsx b/app/react/kubernetes/cluster/NodeView/NodeView.tsx index 3770e2e9d..7fd5c7054 100644 --- a/app/react/kubernetes/cluster/NodeView/NodeView.tsx +++ b/app/react/kubernetes/cluster/NodeView/NodeView.tsx @@ -6,7 +6,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { PageHeader } from '@@/PageHeader'; import { Widget, WidgetBody, WidgetTabs } from '@@/Widget'; -import { findSelectedTabIndex, Tab } from '@@/Widget/WidgetTabs'; +import { Tab, useCurrentTabIndex } from '@@/Widget/WidgetTabs'; import { Badge } from '@@/Badge'; import { Icon } from '@@/Icon'; @@ -54,7 +54,7 @@ export function NodeView() { environmentId, ] ); - const currentTabIndex = findSelectedTabIndex(stateAndParams, tabs); + const currentTabIndex = useCurrentTabIndex(tabs); return ( <>