diff --git a/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.js b/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.js
index 1ade5a556..ca6677e1e 100644
--- a/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.js
+++ b/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.js
@@ -30,11 +30,15 @@ function UserAndTeamAccessAdd({
apiModel,
onClose,
onError,
+ resourceId,
}) {
const { t } = useLingui();
const [selectedResourceType, setSelectedResourceType] = useState(null);
const [stepIdReached, setStepIdReached] = useState(1);
- const { id: userId } = useParams();
+ const { id: routeId } = useParams();
+ // The caller passes the resource id explicitly (works whether the parent
+ // screen uses react-router v5 or v6); fall back to the v5 route param.
+ const associationId = resourceId ?? routeId;
const teamsRouteMatch = useRouteMatch({
path: '/teams/:id/roles',
exact: true,
@@ -292,14 +296,16 @@ function UserAndTeamAccessAdd({
rolesSelected.map((role) =>
resourceRolesTypes.forEach((rolename) => {
if (rolename.name === role.name) {
- roleRequests.push(apiModel.associateRole(userId, rolename.id));
+ roleRequests.push(
+ apiModel.associateRole(associationId, rolename.id)
+ );
}
})
);
await Promise.all(roleRequests);
onFetchData();
- }, [onFetchData, rolesSelected, apiModel, userId, resourcesSelected]),
+ }, [onFetchData, rolesSelected, apiModel, associationId, resourcesSelected]),
{}
);
diff --git a/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js b/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js
index 377c1cda3..b82e02b33 100644
--- a/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js
+++ b/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js
@@ -59,6 +59,7 @@ describe('', () => {
wrapper = mountWithContexts(
{}}
onClose={onClose}
title="Add user permissions"
@@ -171,7 +172,12 @@ describe('', () => {
wrapper.find('Button[type="submit"]').prop('onClick')()
);
- await expect(UsersAPI.associateRole).toHaveBeenCalled();
+ // associate must use the resourceId passed by the parent screen, not a
+ // route param (which is empty when the parent screen uses react-router v6)
+ await expect(UsersAPI.associateRole).toHaveBeenCalledWith(
+ 99,
+ expect.any(Number)
+ );
});
test('should close wizard on cancel', async () => {
diff --git a/awx/ui/src/screens/Team/Team.js b/awx/ui/src/screens/Team/Team.js
index 8ca1efa5f..b63275a01 100644
--- a/awx/ui/src/screens/Team/Team.js
+++ b/awx/ui/src/screens/Team/Team.js
@@ -1,14 +1,14 @@
import React, { useState, useEffect } 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 { CaretLeftIcon } from '@patternfly/react-icons';
import { Card, PageSection } from '@patternfly/react-core';
import { Config } from 'contexts/Config';
@@ -87,42 +87,47 @@ function Team({ setBreadcrumb }) {
{showCardHeader && }
-
-
+
+ } />
{team && (
-
-
-
+ } />
)}
+ {team && } />}
{team && (
-
-
-
+
+ }
+ />
)}
{team && (
-
-
-
+
+ {({ me }) => (
+ <>{me && }>
+ )}
+
+ }
+ />
)}
- {team && (
-
-
- {({ me }) => <>{me && }>}
-
-
- )}
-
- {!hasContentLoading && (
-
- {id && (
-
- {t`View Team Details`}
-
- )}
-
- )}
-
-
+
+ {id && (
+
+ {t`View Team Details`}
+
+ )}
+
+ ) : null
+ }
+ />
+
);
diff --git a/awx/ui/src/screens/Team/Team.test.js b/awx/ui/src/screens/Team/Team.test.js
index c634fee1e..212855044 100644
--- a/awx/ui/src/screens/Team/Team.test.js
+++ b/awx/ui/src/screens/Team/Team.test.js
@@ -1,83 +1,116 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
+import { screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
+import { Routes, Route } from 'react-router-dom-v5-compat';
import { TeamsAPI } from 'api';
-import {
- mountWithContexts,
- waitForElement,
-} from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import Team from './Team';
-jest.mock('../../api');
+jest.mock('../../api/models/Teams');
-const mockMe = {
- is_super_user: true,
- is_system_auditor: false,
-};
+// Markers for the routed tab panels, so assertions are about which branch of
+// the nested v6 tree resolves.
+jest.mock('./TeamDetail', () => {
+ const ReactLib = require('react');
+ return {
+ __esModule: true,
+ default: () => ReactLib.createElement('div', null, 'TeamDetail'),
+ };
+});
+jest.mock('./TeamEdit', () => {
+ const ReactLib = require('react');
+ return {
+ __esModule: true,
+ default: () => ReactLib.createElement('div', null, 'TeamEdit'),
+ };
+});
+jest.mock('./TeamRoles', () => {
+ const ReactLib = require('react');
+ return {
+ __esModule: true,
+ default: () => ReactLib.createElement('div', null, 'TeamRoles'),
+ };
+});
+jest.mock('components/ResourceAccessList', () => {
+ const ReactLib = require('react');
+ return {
+ ResourceAccessList: () =>
+ ReactLib.createElement('div', null, 'ResourceAccessList'),
+ };
+});
const mockTeam = {
id: 1,
name: 'Test Team',
summary_fields: {
- organization: {
- id: 1,
- name: 'Default',
- },
+ organization: { id: 1, name: 'Default' },
+ user_capabilities: { edit: true, delete: true },
},
};
-async function getTeams() {
- return {
- count: 1,
- next: null,
- previous: null,
- data: {
- results: [mockTeam],
- },
- };
+// Team uses paths relative to its parent route, so mount it under the same
+// /teams/:id/* route that Teams.js gives it in the app.
+function renderAt(path) {
+ const history = createMemoryHistory({ initialEntries: [path] });
+ return renderWithContexts(
+
+ {}} />} />
+ ,
+ { context: { router: { history } } }
+ );
}
describe('', () => {
- let wrapper;
-
beforeEach(() => {
TeamsAPI.readDetail.mockResolvedValue({ data: mockTeam });
- TeamsAPI.read.mockImplementation(getTeams);
});
- test('initially renders successfully', async () => {
- await act(async () => {
- wrapper = mountWithContexts(
- {}} me={mockMe} />
- );
- });
- expect(wrapper.find('Team').length).toBe(1);
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('fetches the team detail', async () => {
+ renderAt('/teams/1/details');
+ expect(await screen.findByText('TeamDetail')).toBeInTheDocument();
+ // real route params are strings (the old enzyme test mocked a number)
+ expect(TeamsAPI.readDetail).toHaveBeenCalledWith('1');
+ });
+
+ test('renders the edit panel at /edit', async () => {
+ renderAt('/teams/1/edit');
+ expect(await screen.findByText('TeamEdit')).toBeInTheDocument();
+ });
+
+ test('renders the access panel at /access', async () => {
+ renderAt('/teams/1/access');
+ expect(await screen.findByText('ResourceAccessList')).toBeInTheDocument();
+ });
+
+ test('renders the roles panel at /roles', async () => {
+ renderAt('/teams/1/roles');
+ expect(await screen.findByText('TeamRoles')).toBeInTheDocument();
+ });
+
+ test('redirects the index path to details', async () => {
+ const { history } = renderAt('/teams/1');
+ expect(await screen.findByText('TeamDetail')).toBeInTheDocument();
+ await waitFor(() =>
+ expect(history.location.pathname).toBe('/teams/1/details')
+ );
+ });
+
+ test('shows a not-found error on an unknown sub-route', async () => {
+ renderAt('/teams/1/foobar');
+ expect(await screen.findByText('View Team Details')).toBeInTheDocument();
+ expect(screen.queryByText('TeamDetail')).not.toBeInTheDocument();
});
- test('should show content error when user attempts to navigate to erroneous route', async () => {
- const history = createMemoryHistory({
- initialEntries: ['/teams/1/foobar'],
- });
- await act(async () => {
- wrapper = mountWithContexts(
- {}} me={mockMe} />,
- {
- context: {
- router: {
- history,
- route: {
- location: history.location,
- match: {
- params: { id: 1 },
- url: '/teams/1/foobar',
- path: '/teams/1/foobar',
- },
- },
- },
- },
- }
- );
- });
- await waitForElement(wrapper, 'ContentError', (el) => el.length === 1);
+ test('shows a not-found error when the detail request 404s', async () => {
+ const err = new Error('not found');
+ err.response = { status: 404 };
+ TeamsAPI.readDetail.mockRejectedValue(err);
+ renderAt('/teams/1/details');
+ expect(await screen.findByText('Team not found.')).toBeInTheDocument();
+ expect(screen.queryByText('TeamDetail')).not.toBeInTheDocument();
});
});
diff --git a/awx/ui/src/screens/Team/TeamDetail/TeamDetail.js b/awx/ui/src/screens/Team/TeamDetail/TeamDetail.js
index 5a15e9ad1..7aca789d2 100644
--- a/awx/ui/src/screens/Team/TeamDetail/TeamDetail.js
+++ b/awx/ui/src/screens/Team/TeamDetail/TeamDetail.js
@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
-import { Link, useParams } from 'react-router-dom';
-import { useNavigate } from 'react-router-dom-v5-compat';
+import { Link } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom-v5-compat';
import { Button } from '@patternfly/react-core';
import { useLingui } from '@lingui/react/macro';
diff --git a/awx/ui/src/screens/Team/TeamList/TeamList.js b/awx/ui/src/screens/Team/TeamList/TeamList.js
index fc9541b7a..915d86849 100644
--- a/awx/ui/src/screens/Team/TeamList/TeamList.js
+++ b/awx/ui/src/screens/Team/TeamList/TeamList.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 { Card, PageSection } from '@patternfly/react-core';
import { useLingui } from '@lingui/react/macro';
@@ -30,7 +30,6 @@ const QS_CONFIG = getQSConfig('team', {
function TeamList() {
const { t } = useLingui();
const location = useLocation();
- const match = useRouteMatch();
const {
result: {
@@ -156,7 +155,7 @@ function TeamList() {
? [
,
]
: []),
@@ -173,7 +172,7 @@ function TeamList() {
row.id === team.id)}
onSelect={() => handleSelect(team)}
rowIndex={index}
@@ -181,7 +180,7 @@ function TeamList() {
)}
emptyStateControls={
canAdd ? (
-
+
) : null
}
/>
diff --git a/awx/ui/src/screens/Team/TeamRoles/TeamRolesList.js b/awx/ui/src/screens/Team/TeamRoles/TeamRolesList.js
index 8f447d6db..96cacf34c 100644
--- a/awx/ui/src/screens/Team/TeamRoles/TeamRolesList.js
+++ b/awx/ui/src/screens/Team/TeamRoles/TeamRolesList.js
@@ -196,6 +196,7 @@ function TeamRolesList({ me, team }) {
{showAddModal && (
{
setShowAddModal(false);
fetchRoles();
diff --git a/awx/ui/src/screens/Team/Teams.js b/awx/ui/src/screens/Team/Teams.js
index 85ffe24ea..35ebc18cd 100644
--- a/awx/ui/src/screens/Team/Teams.js
+++ b/awx/ui/src/screens/Team/Teams.js
@@ -1,5 +1,5 @@
import React, { useState, useCallback } from 'react';
-import { Route, Switch } from 'react-router-dom';
+import { Routes, Route } from 'react-router-dom-v5-compat';
import { useLingui } from '@lingui/react/macro';
@@ -40,21 +40,24 @@ function Teams() {
return (
<>
-
-
-
-
-
-
-
-
-
-
- {({ me }) => }
-
-
-
-
+
+ } />
+ {/* so the nested route tree can match the rest */}
+ }
+ />
+
+
+ {({ me }) => }
+
+
+ }
+ />
+
>
);
}
diff --git a/awx/ui/src/screens/Team/Teams.test.js b/awx/ui/src/screens/Team/Teams.test.js
index ccb12bbf8..a22c50c88 100644
--- a/awx/ui/src/screens/Team/Teams.test.js
+++ b/awx/ui/src/screens/Team/Teams.test.js
@@ -1,19 +1,57 @@
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 Teams from './Teams';
-jest.mock('../../api');
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
-}));
+jest.mock('../../api/models/Teams');
+
+// 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('./TeamList', () => {
+ const ReactLib = require('react');
+ return {
+ __esModule: true,
+ default: () => ReactLib.createElement('div', null, 'TeamList'),
+ };
+});
+jest.mock('./TeamAdd', () => {
+ const ReactLib = require('react');
+ return {
+ __esModule: true,
+ default: () => ReactLib.createElement('div', null, 'TeamAdd'),
+ };
+});
+jest.mock('./Team', () => {
+ const ReactLib = require('react');
+ return {
+ __esModule: true,
+ default: () => ReactLib.createElement('div', null, 'Team detail'),
+ };
+});
+
+function renderAt(path) {
+ const history = createMemoryHistory({ initialEntries: [path] });
+ return renderWithContexts(, {
+ context: { router: { history } },
+ });
+}
describe('', () => {
- test('initially renders successfully', () => {
- mountWithContexts(
-
- );
+ test('renders the list at /teams', async () => {
+ renderAt('/teams');
+ expect(await screen.findByText('TeamList')).toBeInTheDocument();
+ });
+
+ test('renders the add form at /teams/add', async () => {
+ renderAt('/teams/add');
+ expect(await screen.findByText('TeamAdd')).toBeInTheDocument();
+ expect(screen.queryByText('TeamList')).not.toBeInTheDocument();
+ });
+
+ test('renders the detail subtree at /teams/:id', async () => {
+ renderAt('/teams/1/details');
+ expect(await screen.findByText('Team detail')).toBeInTheDocument();
+ expect(screen.queryByText('TeamList')).not.toBeInTheDocument();
});
});
diff --git a/awx/ui/src/screens/User/UserRoles/UserRolesList.js b/awx/ui/src/screens/User/UserRoles/UserRolesList.js
index 809056452..2768e2917 100644
--- a/awx/ui/src/screens/User/UserRoles/UserRolesList.js
+++ b/awx/ui/src/screens/User/UserRoles/UserRolesList.js
@@ -193,6 +193,7 @@ function UserRolesList({ user }) {
{showAddModal && (
{
setShowAddModal(false);
fetchRoles();