Address PR feedback. Refactors a bit of unit test coverage to move away from testing state. Re-organized some of the structure of the user list tests to be slightly more efficient.

This commit is contained in:
mabashian
2019-11-11 11:57:39 -05:00
parent deb6e58397
commit ab4fba7ce9
13 changed files with 111 additions and 104 deletions

View File

@@ -73,19 +73,13 @@ PasswordField.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
type: PropTypes.string,
validate: PropTypes.func, validate: PropTypes.func,
isRequired: PropTypes.bool, isRequired: PropTypes.bool,
tooltip: PropTypes.node,
tooltipMaxWidth: PropTypes.string,
}; };
PasswordField.defaultProps = { PasswordField.defaultProps = {
type: 'text',
validate: () => {}, validate: () => {},
isRequired: false, isRequired: false,
tooltip: null,
tooltipMaxWidth: '',
}; };
export default withI18n()(PasswordField); export default withI18n()(PasswordField);

View File

@@ -49,7 +49,7 @@ class TeamListItem extends React.Component {
<DataListCell key="organization"> <DataListCell key="organization">
{team.summary_fields.organization && ( {team.summary_fields.organization && (
<Fragment> <Fragment>
<b style={{ marginRight: '20px' }}> <b css={{ marginRight: '20px' }}>
{i18n._(t`Organization`)} {i18n._(t`Organization`)}
</b> </b>
<Link <Link

View File

@@ -18,6 +18,13 @@ import UserTeams from './UserTeams';
import UserTokens from './UserTokens'; import UserTokens from './UserTokens';
import { UsersAPI } from '@api'; import { UsersAPI } from '@api';
const CardHeader = styled(PFCardHeader)`
--pf-c-card--first-child--PaddingTop: 0;
--pf-c-card--child--PaddingLeft: 0;
--pf-c-card--child--PaddingRight: 0;
position: relative;
`;
class User extends Component { class User extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -82,13 +89,6 @@ class User extends Component {
{ name: i18n._(t`Tokens`), link: `${match.url}/tokens`, id: 4 }, { name: i18n._(t`Tokens`), link: `${match.url}/tokens`, id: 4 },
]; ];
const CardHeader = styled(PFCardHeader)`
--pf-c-card--first-child--PaddingTop: 0;
--pf-c-card--child--PaddingLeft: 0;
--pf-c-card--child--PaddingRight: 0;
position: relative;
`;
let cardHeader = ( let cardHeader = (
<CardHeader style={{ padding: 0 }}> <CardHeader style={{ padding: 0 }}>
<RoutedTabs <RoutedTabs

View File

@@ -23,14 +23,14 @@ async function getUsers() {
}; };
} }
describe.only('<User />', () => { describe('<User />', () => {
test('initially renders succesfully', () => { test('initially renders succesfully', () => {
UsersAPI.readDetail.mockResolvedValue({ data: mockDetails }); UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
UsersAPI.read.mockImplementation(getUsers); UsersAPI.read.mockImplementation(getUsers);
mountWithContexts(<User setBreadcrumb={() => {}} me={mockMe} />); mountWithContexts(<User setBreadcrumb={() => {}} me={mockMe} />);
}); });
test('notifications tab shown for admins', async done => { test('notifications tab shown for admins', async () => {
UsersAPI.readDetail.mockResolvedValue({ data: mockDetails }); UsersAPI.readDetail.mockResolvedValue({ data: mockDetails });
UsersAPI.read.mockImplementation(getUsers); UsersAPI.read.mockImplementation(getUsers);
@@ -38,10 +38,9 @@ describe.only('<User />', () => {
<User setBreadcrumb={() => {}} me={mockMe} /> <User setBreadcrumb={() => {}} me={mockMe} />
); );
await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 5); await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 5);
done();
}); });
test('should show content error when user attempts to navigate to erroneous route', async done => { test('should show content error when user attempts to navigate to erroneous route', async () => {
const history = createMemoryHistory({ const history = createMemoryHistory({
initialEntries: ['/users/1/foobar'], initialEntries: ['/users/1/foobar'],
}); });
@@ -64,6 +63,5 @@ describe.only('<User />', () => {
} }
); );
await waitForElement(wrapper, 'ContentError', el => el.length === 1); await waitForElement(wrapper, 'ContentError', el => el.length === 1);
done();
}); });
}); });

View File

@@ -19,7 +19,7 @@ describe('<UserAdd />', () => {
first_name: 'System', first_name: 'System',
last_name: 'Administrator', last_name: 'Administrator',
password: 'password', password: 'password',
useranization: 1, organization: 1,
is_superuser: true, is_superuser: true,
is_system_auditor: false, is_system_auditor: false,
}; };
@@ -63,7 +63,7 @@ describe('<UserAdd />', () => {
first_name: 'System', first_name: 'System',
last_name: 'Administrator', last_name: 'Administrator',
password: 'password', password: 'password',
useranization: 1, organization: 1,
is_superuser: true, is_superuser: true,
is_system_auditor: false, is_system_auditor: false,
}; };

View File

@@ -16,7 +16,7 @@ describe('<UserEdit />', () => {
first_name: 'System', first_name: 'System',
last_name: 'Administrator', last_name: 'Administrator',
password: 'password', password: 'password',
useranization: 1, organization: 1,
is_superuser: true, is_superuser: true,
is_system_auditor: false, is_system_auditor: false,
}; };

View File

@@ -161,10 +161,16 @@ class UsersList extends Component {
isSearchable: true, isSearchable: true,
}, },
{ {
name: i18n._(t`Created`), name: i18n._(t`First Name`),
key: 'created', key: 'first_name',
isSortable: true, isSortable: true,
isNumeric: true, isSearchable: true,
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
isSortable: true,
isSearchable: true,
}, },
]} ]}
renderToolbar={props => ( renderToolbar={props => (

View File

@@ -6,6 +6,8 @@ import UsersList, { _UsersList } from './UserList';
jest.mock('@api'); jest.mock('@api');
let wrapper;
const loadUsers = jest.spyOn(_UsersList.prototype, 'loadUsers');
const mockUsers = [ const mockUsers = [
{ {
id: 1, id: 1,
@@ -79,15 +81,22 @@ const mockUsers = [
}, },
]; ];
describe('<UsersList />', () => { beforeAll(() => {
beforeEach(() => { UsersAPI.read.mockResolvedValue({
UsersAPI.read.mockResolvedValue({ data: {
data: { count: mockUsers.length,
count: mockUsers.length, results: mockUsers,
results: mockUsers, },
}, });
}); });
afterEach(() => {
jest.clearAllMocks();
wrapper.unmount();
});
describe('UsersList with full permissions', () => {
beforeAll(() => {
UsersAPI.readOptions.mockResolvedValue({ UsersAPI.readOptions.mockResolvedValue({
data: { data: {
actions: { actions: {
@@ -98,8 +107,8 @@ describe('<UsersList />', () => {
}); });
}); });
afterEach(() => { beforeEach(() => {
jest.clearAllMocks(); wrapper = mountWithContexts(<UsersList />);
}); });
test('initially renders successfully', () => { test('initially renders successfully', () => {
@@ -111,9 +120,7 @@ describe('<UsersList />', () => {
); );
}); });
test('Users are retrieved from the api and the components finishes loading', async done => { test('Users are retrieved from the api and the components finishes loading', async () => {
const loadUsers = jest.spyOn(_UsersList.prototype, 'loadUsers');
const wrapper = mountWithContexts(<UsersList />);
await waitForElement( await waitForElement(
wrapper, wrapper,
'UsersList', 'UsersList',
@@ -125,54 +132,67 @@ describe('<UsersList />', () => {
'UsersList', 'UsersList',
el => el.state('hasContentLoading') === false el => el.state('hasContentLoading') === false
); );
done();
}); });
test('handleSelect is called when a user list item is selected', async done => { test('Selects one team when row is checked', async () => {
const handleSelect = jest.spyOn(_UsersList.prototype, 'handleSelect');
const wrapper = mountWithContexts(<UsersList />);
await waitForElement( await waitForElement(
wrapper, wrapper,
'UsersList', 'UsersList',
el => el.state('hasContentLoading') === false el => el.state('hasContentLoading') === false
); );
await wrapper expect(
.find('input#select-user-1') wrapper
.closest('DataListCheck') .find('input[type="checkbox"]')
.findWhere(n => n.prop('checked') === true).length
).toBe(0);
wrapper
.find('UserListItem')
.at(0)
.find('DataListCheck')
.props() .props()
.onChange(); .onChange(true);
expect(handleSelect).toBeCalled(); wrapper.update();
await waitForElement( expect(
wrapper, wrapper
'UsersList', .find('input[type="checkbox"]')
el => el.state('selected').length === 1 .findWhere(n => n.prop('checked') === true).length
); ).toBe(1);
done();
}); });
test('handleSelectAll is called when select all checkbox is clicked', async done => { test('Select all checkbox selects and unselects all rows', async () => {
const handleSelectAll = jest.spyOn(_UsersList.prototype, 'handleSelectAll');
const wrapper = mountWithContexts(<UsersList />);
await waitForElement( await waitForElement(
wrapper, wrapper,
'UsersList', 'UsersList',
el => el.state('hasContentLoading') === false el => el.state('hasContentLoading') === false
); );
expect(
wrapper
.find('input[type="checkbox"]')
.findWhere(n => n.prop('checked') === true).length
).toBe(0);
wrapper wrapper
.find('Checkbox#select-all') .find('Checkbox#select-all')
.props() .props()
.onChange(true); .onChange(true);
expect(handleSelectAll).toBeCalled(); wrapper.update();
await waitForElement( expect(
wrapper, wrapper
'UsersList', .find('input[type="checkbox"]')
el => el.state('selected').length === 2 .findWhere(n => n.prop('checked') === true).length
); ).toBe(3);
done(); wrapper
.find('Checkbox#select-all')
.props()
.onChange(false);
wrapper.update();
expect(
wrapper
.find('input[type="checkbox"]')
.findWhere(n => n.prop('checked') === true).length
).toBe(0);
}); });
test('delete button is disabled if user does not have delete capabilities on a selected user', async done => { test('delete button is disabled if user does not have delete capabilities on a selected user', async () => {
const wrapper = mountWithContexts(<UsersList />);
wrapper.find('UsersList').setState({ wrapper.find('UsersList').setState({
users: mockUsers, users: mockUsers,
itemCount: 2, itemCount: 2,
@@ -192,12 +212,10 @@ describe('<UsersList />', () => {
'ToolbarDeleteButton * button', 'ToolbarDeleteButton * button',
el => el.getDOMNode().disabled === true el => el.getDOMNode().disabled === true
); );
done();
}); });
test('api is called to delete users for each selected user.', () => { test('api is called to delete users for each selected user.', () => {
UsersAPI.destroy = jest.fn(); UsersAPI.destroy = jest.fn();
const wrapper = mountWithContexts(<UsersList />);
wrapper.find('UsersList').setState({ wrapper.find('UsersList').setState({
users: mockUsers, users: mockUsers,
itemCount: 2, itemCount: 2,
@@ -209,7 +227,7 @@ describe('<UsersList />', () => {
expect(UsersAPI.destroy).toHaveBeenCalledTimes(2); expect(UsersAPI.destroy).toHaveBeenCalledTimes(2);
}); });
test('error is shown when user not successfully deleted from api', async done => { test('error is shown when user not successfully deleted from api', async () => {
UsersAPI.destroy.mockRejectedValue( UsersAPI.destroy.mockRejectedValue(
new Error({ new Error({
response: { response: {
@@ -221,7 +239,6 @@ describe('<UsersList />', () => {
}, },
}) })
); );
const wrapper = mountWithContexts(<UsersList />);
wrapper.find('UsersList').setState({ wrapper.find('UsersList').setState({
users: mockUsers, users: mockUsers,
itemCount: 1, itemCount: 1,
@@ -235,12 +252,9 @@ describe('<UsersList />', () => {
'Modal', 'Modal',
el => el.props().isOpen === true && el.props().title === 'Error!' el => el.props().isOpen === true && el.props().title === 'Error!'
); );
done();
}); });
test('Add button shown for users without ability to POST', async done => { test('Add button shown for users with ability to POST', async () => {
const wrapper = mountWithContexts(<UsersList />);
await waitForElement( await waitForElement(
wrapper, wrapper,
'UsersList', 'UsersList',
@@ -252,10 +266,11 @@ describe('<UsersList />', () => {
el => el.state('hasContentLoading') === false el => el.state('hasContentLoading') === false
); );
expect(wrapper.find('ToolbarAddButton').length).toBe(1); expect(wrapper.find('ToolbarAddButton').length).toBe(1);
done();
}); });
});
test('Add button hidden for users without ability to POST', async done => { describe('UsersList without full permissions', () => {
test('Add button hidden for users without ability to POST', async () => {
UsersAPI.readOptions.mockResolvedValue({ UsersAPI.readOptions.mockResolvedValue({
data: { data: {
actions: { actions: {
@@ -263,7 +278,8 @@ describe('<UsersList />', () => {
}, },
}, },
}); });
const wrapper = mountWithContexts(<UsersList />);
wrapper = mountWithContexts(<UsersList />);
await waitForElement( await waitForElement(
wrapper, wrapper,
'UsersList', 'UsersList',
@@ -275,6 +291,5 @@ describe('<UsersList />', () => {
el => el.state('hasContentLoading') === false el => el.state('hasContentLoading') === false
); );
expect(wrapper.find('ToolbarAddButton').length).toBe(0); expect(wrapper.find('ToolbarAddButton').length).toBe(0);
done();
}); });
}); });

View File

@@ -49,9 +49,7 @@ class UserListItem extends React.Component {
<DataListCell key="first-name"> <DataListCell key="first-name">
{user.first_name && ( {user.first_name && (
<Fragment> <Fragment>
<b style={{ marginRight: '20px' }}> <b css={{ marginRight: '20px' }}>{i18n._(t`First Name`)}</b>
{i18n._(t`First Name`)}
</b>
{user.first_name} {user.first_name}
</Fragment> </Fragment>
)} )}
@@ -59,9 +57,7 @@ class UserListItem extends React.Component {
<DataListCell key="last-name"> <DataListCell key="last-name">
{user.last_name && ( {user.last_name && (
<Fragment> <Fragment>
<b style={{ marginRight: '20px' }}> <b css={{ marginRight: '20px' }}>{i18n._(t`Last Name`)}</b>
{i18n._(t`Last Name`)}
</b>
{user.last_name} {user.last_name}
</Fragment> </Fragment>
)} )}

View File

@@ -7,9 +7,15 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
import mockDetails from '../data.user.json'; import mockDetails from '../data.user.json';
import UserListItem from './UserListItem'; import UserListItem from './UserListItem';
describe('<UserListItem />', () => { let wrapper;
test('initially renders succesfully', () => {
mountWithContexts( afterEach(() => {
wrapper.unmount();
});
describe('UserListItem with full permissions', () => {
beforeEach(() => {
wrapper = mountWithContexts(
<I18nProvider> <I18nProvider>
<MemoryRouter initialEntries={['/users']} initialIndex={0}> <MemoryRouter initialEntries={['/users']} initialIndex={0}>
<UserListItem <UserListItem
@@ -22,23 +28,17 @@ describe('<UserListItem />', () => {
</I18nProvider> </I18nProvider>
); );
}); });
test('initially renders succesfully', () => {
expect(wrapper.length).toBe(1);
});
test('edit button shown to users with edit capabilities', () => { test('edit button shown to users with edit capabilities', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/users']} initialIndex={0}>
<UserListItem
user={mockDetails}
detailUrl="/user/1"
isSelected
onSelect={() => {}}
/>
</MemoryRouter>
</I18nProvider>
);
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
}); });
});
describe('UserListItem without full permissions', () => {
test('edit button hidden from users without edit capabilities', () => { test('edit button hidden from users without edit capabilities', () => {
const wrapper = mountWithContexts( wrapper = mountWithContexts(
<I18nProvider> <I18nProvider>
<MemoryRouter initialEntries={['/users']} initialIndex={0}> <MemoryRouter initialEntries={['/users']} initialIndex={0}>
<UserListItem <UserListItem

View File

@@ -6,7 +6,7 @@ import { Formik, Field } from 'formik';
import { Form, FormGroup } from '@patternfly/react-core'; import { Form, FormGroup } from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
import FormField, { FieldTooltip, PasswordField } from '@components/FormField'; import FormField, { PasswordField } from '@components/FormField';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup';
import { required, requiredEmail } from '@util/validators'; import { required, requiredEmail } from '@util/validators';
@@ -169,9 +169,8 @@ function UserForm(props) {
helperTextInvalid={form.errors.user_type} helperTextInvalid={form.errors.user_type}
isRequired isRequired
isValid={isValid} isValid={isValid}
label={i18n._(t`Job Type`)} label={i18n._(t`User Type`)}
> >
<FieldTooltip content={i18n._(t`Fill me in.`)} />
<AnsibleSelect <AnsibleSelect
isValid={isValid} isValid={isValid}
id="user-type" id="user-type"

View File

@@ -53,7 +53,7 @@ describe('<UserForm />', () => {
expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Password"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Confirm Password"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Confirm Password"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
expect(wrapper.find('FormGroup[label="Job Type"]').length).toBe(1); expect(wrapper.find('FormGroup[label="User Type"]').length).toBe(1);
}); });
test('edit form hides org field', async () => { test('edit form hides org field', async () => {

View File

@@ -176,7 +176,6 @@ export const Job = shape({
artifacts: shape({}), artifacts: shape({}),
}); });
<<<<<<< HEAD
export const Host = shape({ export const Host = shape({
id: number.isRequired, id: number.isRequired,
type: oneOf(['host']), type: oneOf(['host']),