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();
});
});