diff --git a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironment.js b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironment.js index bf3f1c1be..6f25f690b 100644 --- a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironment.js +++ b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironment.js @@ -1,12 +1,12 @@ import React, { useEffect, useCallback } from 'react'; +import { Link } from 'react-router-dom'; import { - Link, - Redirect, + Routes, Route, - Switch, + Navigate, useLocation, useParams, -} from 'react-router-dom'; +} from 'react-router-dom-v5-compat'; import { useLingui } from '@lingui/react/macro'; import { Card, PageSection } from '@patternfly/react-core'; @@ -104,32 +104,33 @@ function ExecutionEnvironment({ setBreadcrumb }) { {cardHeader} {isLoading && } {!isLoading && executionEnvironment && ( - - + } /> + + } /> - {executionEnvironment && ( - <> - - - - - - - - - - - )} - + + } + /> + + } + /> + )} diff --git a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironment.test.js b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironment.test.js new file mode 100644 index 000000000..5012594ea --- /dev/null +++ b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironment.test.js @@ -0,0 +1,119 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { Routes, Route } from 'react-router-dom-v5-compat'; +import { ExecutionEnvironmentsAPI } from 'api'; +import { renderWithContexts } from '../../../testUtils/rtlContexts'; +import ExecutionEnvironment from './ExecutionEnvironment'; + +jest.mock('../../api/models/ExecutionEnvironments'); + +// Markers for the routed tab panels, so assertions are about which branch of +// the nested v6 tree resolves. +jest.mock('./ExecutionEnvironmentDetails', () => { + const ReactLib = require('react'); + return { + __esModule: true, + default: () => + ReactLib.createElement('div', null, 'ExecutionEnvironmentDetails'), + }; +}); +jest.mock('./ExecutionEnvironmentEdit', () => { + const ReactLib = require('react'); + return { + __esModule: true, + default: () => + ReactLib.createElement('div', null, 'ExecutionEnvironmentEdit'), + }; +}); +jest.mock('./ExecutionEnvironmentTemplate', () => { + const ReactLib = require('react'); + return { + __esModule: true, + default: () => + ReactLib.createElement('div', null, 'ExecutionEnvironmentTemplateList'), + }; +}); + +const executionEnvironment = { + id: 42, + name: 'Foo', + image: 'quay.io/foo/bar', + summary_fields: { user_capabilities: { edit: true, delete: true } }, +}; + +// ExecutionEnvironment uses paths relative to its parent route, so mount it +// under the same /execution_environments/:id/* route that +// ExecutionEnvironments.js gives it in the app. +function renderAt(path) { + const history = createMemoryHistory({ initialEntries: [path] }); + return renderWithContexts( + + {}} />} + /> + , + { context: { router: { history } } } + ); +} + +describe('', () => { + beforeEach(() => { + ExecutionEnvironmentsAPI.readDetail.mockResolvedValue({ + data: executionEnvironment, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('fetches the execution environment detail', async () => { + renderAt('/execution_environments/42/details'); + expect( + await screen.findByText('ExecutionEnvironmentDetails') + ).toBeInTheDocument(); + // real route params are strings (the old enzyme test mocked a number) + expect(ExecutionEnvironmentsAPI.readDetail).toHaveBeenCalledWith('42'); + }); + + test('renders the edit panel at /edit', async () => { + renderAt('/execution_environments/42/edit'); + expect( + await screen.findByText('ExecutionEnvironmentEdit') + ).toBeInTheDocument(); + }); + + test('renders the templates panel at /templates', async () => { + renderAt('/execution_environments/42/templates'); + expect( + await screen.findByText('ExecutionEnvironmentTemplateList') + ).toBeInTheDocument(); + }); + + test('redirects the index path to details', async () => { + const { history } = renderAt('/execution_environments/42'); + expect( + await screen.findByText('ExecutionEnvironmentDetails') + ).toBeInTheDocument(); + await waitFor(() => + expect(history.location.pathname).toBe( + '/execution_environments/42/details' + ) + ); + }); + + test('shows a not-found error when the detail request 404s', async () => { + const err = new Error('not found'); + err.response = { status: 404 }; + ExecutionEnvironmentsAPI.readDetail.mockRejectedValue(err); + renderAt('/execution_environments/42/details'); + expect( + await screen.findByText('Execution environment not found.') + ).toBeInTheDocument(); + expect( + screen.queryByText('ExecutionEnvironmentDetails') + ).not.toBeInTheDocument(); + }); +}); diff --git a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.js b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.js index 14ad6070e..14cf91ff3 100644 --- a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.js +++ b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironmentList/ExecutionEnvironmentList.js @@ -1,5 +1,5 @@ import React, { useEffect, useCallback } from 'react'; -import { useLocation, useRouteMatch } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useLingui } from '@lingui/react/macro'; import { Card, PageSection } from '@patternfly/react-core'; @@ -30,7 +30,6 @@ const QS_CONFIG = getQSConfig('execution_environments', { function ExecutionEnvironmentList() { const { t } = useLingui(); const location = useLocation(); - const match = useRouteMatch(); const { addToast, Toast, toastProps } = useToast(); const { @@ -182,7 +181,7 @@ function ExecutionEnvironmentList() { , ] : []), @@ -206,7 +205,7 @@ function ExecutionEnvironmentList() { key={executionEnvironment.id} rowIndex={index} executionEnvironment={executionEnvironment} - detailUrl={`${match.url}/${executionEnvironment.id}/details`} + detailUrl={`/execution_environments/${executionEnvironment.id}/details`} onSelect={() => handleSelect(executionEnvironment)} onCopy={handleCopy} isSelected={selected.some( @@ -217,7 +216,7 @@ function ExecutionEnvironmentList() { )} emptyStateControls={ canAdd && ( - + ) } /> diff --git a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.js b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.js index 72162a584..0ef6eb4f5 100644 --- a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.js +++ b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.js @@ -1,6 +1,6 @@ import React, { useState, useCallback } from 'react'; import { useLingui } from '@lingui/react/macro'; -import { Route, Switch } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom-v5-compat'; import PersistentFilters from 'components/PersistentFilters'; import ScreenHeader from 'components/ScreenHeader/ScreenHeader'; import ExecutionEnvironment from './ExecutionEnvironment'; @@ -35,19 +35,27 @@ function ExecutionEnvironments() { streamType="execution_environment" breadcrumbConfig={breadcrumbConfig} /> - - - - - - - - - - - - - + + } + /> + {/* so the nested route tree can match the rest */} + + } + /> + + + + } + /> + ); } diff --git a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.js b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.js index e036174dc..3b1dc5d20 100644 --- a/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.js +++ b/awx/ui/src/screens/ExecutionEnvironment/ExecutionEnvironments.test.js @@ -1,21 +1,71 @@ import React from 'react'; - -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { screen } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { renderWithContexts } from '../../../testUtils/rtlContexts'; import ExecutionEnvironments from './ExecutionEnvironments'; -describe('', () => { - let pageWrapper; - let pageSections; +jest.mock('../../api/models/ExecutionEnvironments'); + +// Replace the routed children with markers so the assertions are purely about +// which branch of the v6 tree resolves for a given URL. +jest.mock('./ExecutionEnvironmentList', () => { + const ReactLib = require('react'); + return { + __esModule: true, + default: () => + ReactLib.createElement('div', null, 'ExecutionEnvironmentList'), + }; +}); +jest.mock('./ExecutionEnvironmentAdd', () => { + const ReactLib = require('react'); + return { + __esModule: true, + default: () => + ReactLib.createElement('div', null, 'ExecutionEnvironmentAdd'), + }; +}); +jest.mock('./ExecutionEnvironment', () => { + const ReactLib = require('react'); + return { + __esModule: true, + default: () => + ReactLib.createElement('div', null, 'ExecutionEnvironment detail'), + }; +}); + +function renderAt(path) { + const history = createMemoryHistory({ initialEntries: [path] }); + return renderWithContexts(, { + context: { router: { history } }, + }); +} + +describe('', () => { + test('renders the list at /execution_environments', async () => { + renderAt('/execution_environments'); + expect( + await screen.findByText('ExecutionEnvironmentList') + ).toBeInTheDocument(); + }); - beforeEach(() => { - pageWrapper = mountWithContexts(); - pageSections = pageWrapper.find('PageSection'); + test('renders the add form at /execution_environments/add', async () => { + renderAt('/execution_environments/add'); + expect( + await screen.findByText('ExecutionEnvironmentAdd') + ).toBeInTheDocument(); + expect( + screen.queryByText('ExecutionEnvironmentList') + ).not.toBeInTheDocument(); }); - test('initially renders without crashing', () => { - expect(pageWrapper.length).toBe(1); - expect(pageSections.length).toBe(1); - expect(pageSections.first().props().variant).toBe('light'); + test('renders the detail subtree at /execution_environments/:id', async () => { + renderAt('/execution_environments/42/details'); + expect( + await screen.findByText('ExecutionEnvironment detail') + ).toBeInTheDocument(); + expect( + screen.queryByText('ExecutionEnvironmentList') + ).not.toBeInTheDocument(); }); });