diff --git a/src/components/Workspace/__tests__/workspace-context.spec.tsx b/src/components/Workspace/__tests__/workspace-context.spec.tsx new file mode 100644 index 0000000..60bbc17 --- /dev/null +++ b/src/components/Workspace/__tests__/workspace-context.spec.tsx @@ -0,0 +1,207 @@ +import { useContext } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { render, screen, waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { createReactRouterMock } from '../../../utils/test-utils'; +import { getLastUsedWorkspace, setLastUsedWorkspace } from '../utils'; +import { WorkspaceProvider, WorkspaceContext } from '../workspace-context'; + +jest.mock('@tanstack/react-query', () => ({ + ...jest.requireActual('@tanstack/react-query'), + useQuery: jest.fn(), +})); + +jest.mock('../utils', () => ({ + ...jest.requireActual('../utils'), + createWorkspaceQueryOptions: jest.fn(), + getLastUsedWorkspace: jest.fn(), + setLastUsedWorkspace: jest.fn(), +})); + +// Test data +const mockWorkspaces = [ + { + metadata: { + name: 'workspace-1', + }, + status: { + namespaces: [{ type: 'default', name: 'test-namespace' }], + type: 'home', + }, + }, + { + metadata: { + name: 'workspace-2', + }, + status: { + namespaces: [{ type: 'default', name: 'test-namespace-2' }], + }, + }, +]; + +const mockNamespace = 'test-namespace'; + +const mockUseNavigate = createReactRouterMock('useNavigate'); +const mockUseParams = createReactRouterMock('useParams'); +const mockUseQuery = useQuery as jest.Mock; +const mockGetLastUsedWorkspace = getLastUsedWorkspace as jest.Mock; + +describe('WorkspaceProvider', () => { + const mockNavigate = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + mockUseNavigate.mockReturnValue(mockNavigate); + mockUseParams.mockReturnValue({}); + mockGetLastUsedWorkspace.mockReturnValue('workspace-1'); + }); + + it('should renders loading spinner when data is being fetched', () => { + mockUseQuery + .mockReturnValueOnce({ + data: undefined, + isLoading: true, + }) + .mockReturnValueOnce({ + data: undefined, + isLoading: true, + }); + + render( + +
Child content
+
, + ); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + expect(screen.queryByText('Child content')).not.toBeInTheDocument(); + }); + + it('should renders children when data is loaded', async () => { + mockUseQuery + .mockReturnValueOnce({ + data: mockWorkspaces, + isLoading: false, + }) + .mockReturnValue({ + data: mockWorkspaces[0], + isLoading: false, + }); + + render( + +
Child content
+
, + ); + + await waitFor(() => { + expect(screen.getByText('Child content')).toBeInTheDocument(); + }); + }); + + it('handles error state correctly', () => { + const errorMessage = 'Failed to load workspace'; + mockUseQuery + .mockReturnValueOnce({ + data: mockWorkspaces, + isLoading: false, + }) + .mockReturnValueOnce({ + error: new Error(errorMessage), + isLoading: false, + }); + + render( + +
Child content
+
, + ); + + expect(screen.getByText(`Unable to access workspace workspace-1`)).toBeInTheDocument(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + + it('provides correct context values', async () => { + mockUseQuery + .mockReturnValueOnce({ + data: mockWorkspaces, + isLoading: false, + }) + .mockReturnValueOnce({ + data: mockWorkspaces[0], + isLoading: false, + }); + + const TestConsumer = () => { + const context = useContext(WorkspaceContext); + return ( +
+
{context.namespace}
+
{context.workspace}
+
{String(context.workspacesLoaded)}
+
+ ); + }; + + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByTestId('namespace')).toHaveTextContent(mockNamespace); + expect(screen.getByTestId('workspace')).toHaveTextContent('workspace-1'); + expect(screen.getByTestId('workspaces-loaded')).toHaveTextContent('true'); + }); + }); + + it('updates last used workspace when active workspace changes', async () => { + mockUseQuery + .mockReturnValueOnce({ + data: mockWorkspaces, + isLoading: false, + }) + .mockReturnValueOnce({ + data: mockWorkspaces[0], + isLoading: false, + }); + + mockUseParams.mockReturnValue({ workspaceName: 'workspace-2' }); + + render( + +
Child content
+
, + ); + + await waitFor(() => { + expect(setLastUsedWorkspace).toHaveBeenCalledWith('workspace-2'); + }); + }); + + it('navigates to home workspace when error occurs and home button is clicked', async () => { + const errorMessage = 'Failed to load workspace'; + mockUseQuery + .mockReturnValueOnce({ + data: mockWorkspaces, + isLoading: false, + }) + .mockReturnValueOnce({ + error: new Error(errorMessage), + isLoading: false, + }); + + render( + +
Child content
+
, + ); + + const homeButton = screen.getByText('Go to workspace-1 workspace'); + await userEvent.click(homeButton); + + expect(setLastUsedWorkspace).toHaveBeenCalledWith('workspace-1'); + expect(mockNavigate).toHaveBeenCalledWith('/workspaces/workspace-1/applications'); + }); +}); diff --git a/src/components/Workspace/workspace-context.tsx b/src/components/Workspace/workspace-context.tsx index 1909ed9..bab2045 100644 --- a/src/components/Workspace/workspace-context.tsx +++ b/src/components/Workspace/workspace-context.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { useParams } from 'react-router-dom'; -import { Bullseye, Spinner } from '@patternfly/react-core'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Bullseye, Button, Spinner } from '@patternfly/react-core'; import { useQuery } from '@tanstack/react-query'; import { RouterParams } from '../../routes/utils'; +import ErrorEmptyState from '../../shared/components/empty-state/ErrorEmptyState'; import { Workspace } from '../../types'; import { createWorkspaceQueryOptions, @@ -33,26 +34,52 @@ export const WorkspaceContext = React.createContext({ export const WorkspaceProvider: React.FC = ({ children }) => { const { data: workspaces, isLoading: workspaceLoading } = useQuery(createWorkspaceQueryOptions()); const params = useParams(); + const navigate = useNavigate(); + + const homeWorkspace = React.useMemo( + () => (!workspaceLoading ? getHomeWorkspace(workspaces) ?? workspaces[0] : null), + [workspaces, workspaceLoading], + ); const activeWorkspaceName = - params.workspaceName ?? - getLastUsedWorkspace() ?? - getHomeWorkspace(workspaces)?.metadata?.name ?? - workspaces[0]?.metadata?.name; + params.workspaceName ?? getLastUsedWorkspace() ?? homeWorkspace?.metadata?.name; - const { data: workspaceResource, isLoading: activeWorkspaceLoading } = useQuery( - createWorkspaceQueryOptions(activeWorkspaceName), - ); + const { + data: workspaceResource, + isLoading: activeWorkspaceLoading, + error, + } = useQuery({ ...createWorkspaceQueryOptions(activeWorkspaceName), retry: false }); const namespace = !activeWorkspaceLoading ? getDefaultNsForWorkspace(workspaceResource)?.name : undefined; React.useEffect(() => { - if (getLastUsedWorkspace() !== activeWorkspaceName) { + if (!error && getLastUsedWorkspace() !== activeWorkspaceName) { setLastUsedWorkspace(activeWorkspaceName); } - }, [activeWorkspaceName]); + }, [activeWorkspaceName, error]); + + if (error) { + return ( + + {homeWorkspace ? ( + + ) : null} + + ); + } return ( = ({ children lastUsedWorkspace: getLastUsedWorkspace(), }} > - {!(workspaceLoading && activeWorkspaceLoading) ? ( + {!(workspaceLoading || activeWorkspaceLoading) ? ( children ) : ( diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ccb295c..a4088d0 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -113,7 +113,7 @@ export const router = createBrowserRouter([ element: , }, { - path: `/workspaces/:${RouterParams.workspaceName}/applications`, + path: `workspaces/:${RouterParams.workspaceName}/applications`, loader: applicationPageLoader, element: , errorElement: ,