1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 08:21:15 +03:00

Integrate proptypes for our shared components.

- Fix unit tests.
- Fix linter errors.
This commit is contained in:
kialam 2019-02-15 15:08:52 -05:00
parent 91f87b6d81
commit b340d49cb7
No known key found for this signature in database
GPG Key ID: 2D0E60E4B8C7EA0F
26 changed files with 313 additions and 55 deletions

View File

@ -6,11 +6,11 @@ import About from '../../src/components/About';
describe('<About />', () => {
let aboutWrapper;
let closeButton;
const onClose = jest.fn();
test('initially renders without crashing', () => {
aboutWrapper = mount(
<I18nProvider>
<About isOpen />
<About isOpen onClose={onClose} />
</I18nProvider>
);
expect(aboutWrapper.length).toBe(1);
@ -18,7 +18,6 @@ describe('<About />', () => {
});
test('close button calls onClose handler', () => {
const onClose = jest.fn();
aboutWrapper = mount(
<I18nProvider>
<About isOpen onClose={onClose} />

View File

@ -3,7 +3,7 @@ import { mount } from 'enzyme';
import { I18nProvider } from '@lingui/react';
import Lookup from '../../src/components/Lookup';
let mockData = [{ name: 'foo', id: 1, isChecked: false }];
let mockData = [{ name: 'foo', id: 1 }];
describe('<Lookup />', () => {
test('initially renders succesfully', () => {
mount(
@ -85,7 +85,7 @@ describe('<Lookup />', () => {
});
test('calls "toggleSelected" when remove icon is clicked', () => {
const spy = jest.spyOn(Lookup.prototype, 'toggleSelected');
mockData = [{ name: 'foo', id: 1, isChecked: false }, { name: 'bar', id: 2, isChecked: true }];
mockData = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
const wrapper = mount(
<I18nProvider>
<Lookup
@ -103,20 +103,21 @@ describe('<Lookup />', () => {
});
test('"wrapTags" method properly handles data', () => {
const spy = jest.spyOn(Lookup.prototype, 'wrapTags');
mockData = [{ name: 'foo', id: 0, isChecked: false }, { name: 'bar', id: 1, isChecked: false }];
mockData = [{ name: 'foo', id: 0 }, { name: 'bar', id: 1 }];
const wrapper = mount(
<I18nProvider>
<Lookup
lookup_header="Foo Bar"
onLookupSave={() => { }}
data={mockData}
value={mockData}
selected={[]}
getItems={() => { }}
/>
</I18nProvider>
);
expect(spy).toHaveBeenCalled();
const pill = wrapper.find('span.awx-c-tag--pill');
expect(pill).toHaveLength(0);
expect(pill).toHaveLength(2);
});
test('toggleSelected successfully adds/removes row from lookupSelectedItems state', () => {
mockData = [{ name: 'foo', id: 1 }];
@ -125,8 +126,9 @@ describe('<Lookup />', () => {
<Lookup
lookup_header="Foo Bar"
onLookupSave={() => { }}
data={mockData}
value={mockData}
selected={[]}
getItems={() => { }}
/>
</I18nProvider>
).find('Lookup');

View File

@ -112,9 +112,9 @@ describe('<Notifications />', () => {
const getNotificationsFn = jest.fn().mockResolvedValue({
data: {
results: [
{ id: 1 },
{ id: 2 },
{ id: 3 }
{ id: 1, notification_type: 'slack' },
{ id: 2, notification_type: 'email' },
{ id: 3, notification_type: 'github' }
]
}
});

View File

@ -6,6 +6,7 @@ import NotificationListItem from '../../src/components/NotificationsList/Notific
describe('<NotificationListItem />', () => {
let wrapper;
const toggleNotification = jest.fn();
afterEach(() => {
if (wrapper) {
@ -18,7 +19,12 @@ describe('<NotificationListItem />', () => {
wrapper = mount(
<I18nProvider>
<MemoryRouter>
<NotificationListItem />
<NotificationListItem
itemId={9000}
toggleNotification={toggleNotification}
detailUrl="/foo"
notificationType="slack"
/>
</MemoryRouter>
</I18nProvider>
);
@ -26,7 +32,6 @@ describe('<NotificationListItem />', () => {
});
test('handles success click when toggle is on', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
@ -34,6 +39,8 @@ describe('<NotificationListItem />', () => {
itemId={9000}
successTurnedOn
toggleNotification={toggleNotification}
detailUrl="/foo"
notificationType="slack"
/>
</MemoryRouter>
</I18nProvider>
@ -43,7 +50,6 @@ describe('<NotificationListItem />', () => {
});
test('handles success click when toggle is off', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
@ -51,6 +57,8 @@ describe('<NotificationListItem />', () => {
itemId={9000}
successTurnedOn={false}
toggleNotification={toggleNotification}
detailUrl="/foo"
notificationType="slack"
/>
</MemoryRouter>
</I18nProvider>
@ -60,7 +68,6 @@ describe('<NotificationListItem />', () => {
});
test('handles error click when toggle is on', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
@ -68,6 +75,8 @@ describe('<NotificationListItem />', () => {
itemId={9000}
errorTurnedOn
toggleNotification={toggleNotification}
detailUrl="/foo"
notificationType="slack"
/>
</MemoryRouter>
</I18nProvider>
@ -77,7 +86,6 @@ describe('<NotificationListItem />', () => {
});
test('handles error click when toggle is off', () => {
const toggleNotification = jest.fn();
wrapper = mount(
<I18nProvider>
<MemoryRouter>
@ -85,6 +93,8 @@ describe('<NotificationListItem />', () => {
itemId={9000}
errorTurnedOn={false}
toggleNotification={toggleNotification}
detailUrl="/foo"
notificationType="slack"
/>
</MemoryRouter>
</I18nProvider>

View File

@ -8,17 +8,24 @@ import PageHeaderToolbar from '../../src/components/PageHeaderToolbar';
describe('PageHeaderToolbar', () => {
const pageHelpDropdownSelector = 'Dropdown QuestionCircleIcon';
const pageUserDropdownSelector = 'Dropdown UserIcon';
const onAboutClick = jest.fn();
const onLogoutClick = jest.fn();
test('expected content is rendered on initialization', () => {
const wrapper = mount(<I18nProvider><PageHeaderToolbar /></I18nProvider>);
const wrapper = mount(
<I18nProvider>
<PageHeaderToolbar
onAboutClick={onAboutClick}
onLogoutClick={onLogoutClick}
/>
</I18nProvider>
);
expect(wrapper.find(pageHelpDropdownSelector)).toHaveLength(1);
expect(wrapper.find(pageUserDropdownSelector)).toHaveLength(1);
});
test('dropdowns have expected items and callbacks', () => {
const onAboutClick = jest.fn();
const onLogoutClick = jest.fn();
const wrapper = mount(
<MemoryRouter>
<I18nProvider>

View File

@ -7,14 +7,24 @@ describe('<Tooltip />', () => {
let content;
let mouseOverHandler;
let mouseOutHandler;
const child = (<span>foo</span>);
const message = 'hi';
test('initially renders without crashing', () => {
elem = mount(<Tooltip />);
elem = mount(
<Tooltip message={message}>
{child}
</Tooltip>
);
expect(elem.length).toBe(1);
});
test('shows/hides with mouse over and leave', () => {
elem = mount(<Tooltip />);
elem = mount(
<Tooltip message={message}>
{child}
</Tooltip>
);
mouseOverHandler = elem.find('.mouseOverHandler');
mouseOutHandler = elem.find('.mouseOutHandler');
expect(elem.state().isDisplayed).toBe(false);
@ -34,7 +44,11 @@ describe('<Tooltip />', () => {
});
test('shows/hides with focus and blur', () => {
elem = mount(<Tooltip />);
elem = mount(
<Tooltip message={message}>
{child}
</Tooltip>
);
mouseOverHandler = elem.find('.mouseOverHandler');
mouseOutHandler = elem.find('.mouseOutHandler');
expect(elem.state().isDisplayed).toBe(false);

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import {
@ -86,4 +87,17 @@ class About extends React.Component {
}
}
About.propTypes = {
ansible_version: PropTypes.string,
isOpen: PropTypes.bool,
onClose: PropTypes.func.isRequired,
version: PropTypes.string,
};
About.defaultProps = {
ansible_version: null,
isOpen: false,
version: null,
};
export default About;

View File

@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
FormGroup,
Select,
SelectOption,
} from '@patternfly/react-core';
@ -32,15 +34,17 @@ class AnsibleSelect extends React.Component {
render () {
const { count } = this.state;
const { label = '', value, data, defaultSelected } = this.props;
const { label = '', value, data, name, defaultSelected } = this.props;
let elem;
if (count > 1) {
elem = (
<Select value={value} onChange={this.onSelectChange} aria-label="Select Input">
{data.map((datum) => (datum === defaultSelected
? (<SelectOption key="" value="" label={`Use Default ${label}`} />) : (<SelectOption key={datum} value={datum} label={datum} />)))
}
</Select>
<FormGroup label={label} fieldId={`ansible-select-${name}`}>
<Select value={value} onChange={this.onSelectChange} aria-label="Select Input">
{data.map((datum) => (datum === defaultSelected
? (<SelectOption key="" value="" label={`Use Default ${label}`} />) : (<SelectOption key={datum} value={datum} label={datum} />)))
}
</Select>
</FormGroup>
);
} else {
elem = null;
@ -48,4 +52,21 @@ class AnsibleSelect extends React.Component {
return elem;
}
}
AnsibleSelect.defaultProps = {
data: [],
label: 'Ansible Select',
defaultSelected: null,
name: null,
};
AnsibleSelect.propTypes = {
data: PropTypes.arrayOf(PropTypes.string),
defaultSelected: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string,
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
};
export default AnsibleSelect;

View File

@ -1,4 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Chip } from '@patternfly/react-core';
import './basicChip.scss';
@ -10,4 +12,8 @@ const BasicChip = ({ text }) => (
</Chip>
);
BasicChip.propTypes = {
text: PropTypes.string.isRequired,
};
export default BasicChip;

View File

@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
PageSection,
PageSectionVariants,
@ -68,4 +69,12 @@ const Crumb = ({ breadcrumbConfig, match }) => {
);
};
Breadcrumbs.propTypes = {
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
};
Crumb.propTypes = {
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
};
export default withRouter(Breadcrumbs);

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
@ -36,7 +37,6 @@ class DataListToolbar extends React.Component {
super(props);
const { sortedColumnKey } = this.props;
this.state = {
isSearchDropdownOpen: false,
isSortDropdownOpen: false,
@ -282,4 +282,29 @@ class DataListToolbar extends React.Component {
}
}
DataListToolbar.propTypes = {
addUrl: PropTypes.string,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isAllSelected: PropTypes.bool,
onSearch: PropTypes.func,
onSelectAll: PropTypes.func,
onSort: PropTypes.func,
showDelete: PropTypes.bool,
showSelectAll: PropTypes.bool,
sortOrder: PropTypes.string,
sortedColumnKey: PropTypes.string,
};
DataListToolbar.defaultProps = {
addUrl: null,
onSearch: null,
onSelectAll: null,
onSort: null,
showDelete: false,
showSelectAll: false,
sortOrder: 'ascending',
sortedColumnKey: 'name',
isAllSelected: false,
};
export default DataListToolbar;

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
@ -21,7 +22,7 @@ const buttonGroupStyle = {
marginRight: '20px'
};
export default ({ onSubmit, submitDisabled, onCancel }) => (
const FormActionGroup = ({ onSubmit, submitDisabled, onCancel }) => (
<I18n>
{({ i18n }) => (
<ActionGroup style={formActionGroupStyle}>
@ -37,3 +38,15 @@ export default ({ onSubmit, submitDisabled, onCancel }) => (
)}
</I18n>
);
FormActionGroup.propTypes = {
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
submitDisabled: PropTypes.bool,
};
FormActionGroup.defaultProps = {
submitDisabled: true,
};
export default FormActionGroup;

View File

@ -1,11 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Checkbox,
} from '@patternfly/react-core';
export default ({
const CheckboxListItem = ({
itemId,
name,
isSelected,
@ -32,3 +33,12 @@ export default ({
</div>
</li>
);
CheckboxListItem.propTypes = {
itemId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,
};
export default CheckboxListItem;

View File

@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { SearchIcon, CubesIcon } from '@patternfly/react-icons';
import {
Modal,
@ -125,7 +125,7 @@ class Lookup extends React.Component {
render () {
const { isModalOpen, lookupSelectedItems, error, results, count, page, page_size } = this.state;
const { lookupHeader = 'items', value } = this.props;
const { lookupHeader, value } = this.props;
return (
<I18n>
@ -174,6 +174,7 @@ class Lookup extends React.Component {
pageCount={Math.ceil(count / page_size)}
page_size={page_size}
onSetPage={this.onSetPage}
pageSizeOptions={null}
style={paginationStyling}
/>
</Fragment>
@ -194,4 +195,18 @@ class Lookup extends React.Component {
);
}
}
Lookup.propTypes = {
getItems: PropTypes.func.isRequired,
lookupHeader: PropTypes.string,
name: PropTypes.string,
onLookupSave: PropTypes.func.isRequired,
value: PropTypes.arrayOf(PropTypes.object).isRequired,
};
Lookup.defaultProps = {
lookupHeader: 'items',
name: null,
};
export default Lookup;

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
withRouter
} from 'react-router-dom';
@ -55,4 +56,10 @@ class NavExpandableGroup extends Component {
}
}
NavExpandableGroup.propTypes = {
groupId: PropTypes.string.isRequired,
groupTitle: PropTypes.string.isRequired,
routes: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export default withRouter(NavExpandableGroup);

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
@ -20,7 +21,6 @@ class NotificationListItem extends React.Component {
errorTurnedOn,
toggleNotification
} = this.props;
const capText = {
textTransform: 'capitalize'
};
@ -69,5 +69,21 @@ class NotificationListItem extends React.Component {
}
}
NotificationListItem.propTypes = {
detailUrl: PropTypes.string.isRequired,
errorTurnedOn: PropTypes.bool,
itemId: PropTypes.number.isRequired,
name: PropTypes.string,
notificationType: PropTypes.string.isRequired,
successTurnedOn: PropTypes.bool,
toggleNotification: PropTypes.func.isRequired,
};
NotificationListItem.defaultProps = {
errorTurnedOn: false,
name: null,
successTurnedOn: false,
};
export default NotificationListItem;

View File

@ -2,6 +2,7 @@ import React, {
Component,
Fragment
} from 'react';
import PropTypes from 'prop-types';
import { Title, EmptyState, EmptyStateIcon, EmptyStateBody } from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons';
import { I18n, i18nMark } from '@lingui/react';
@ -29,8 +30,6 @@ class Notifications extends Component {
order_by: 'name',
};
pageSizeOptions = [5, 10, 25, 50];
constructor (props) {
super(props);
@ -286,7 +285,6 @@ class Notifications extends Component {
successTemplateIds,
errorTemplateIds
} = this.state;
return (
<Fragment>
{noInitialResults && (
@ -326,8 +324,6 @@ class Notifications extends Component {
name={o.name}
notificationType={o.notification_type}
detailUrl={`/notifications/${o.id}`}
isSelected={selected.includes(o.id)}
onSelect={() => this.onSelect(o.id)}
toggleNotification={this.toggleNotification}
errorTurnedOn={errorTemplateIds.includes(o.id)}
successTurnedOn={successTemplateIds.includes(o.id)}
@ -341,7 +337,6 @@ class Notifications extends Component {
page={page}
pageCount={pageCount}
page_size={page_size}
pageSizeOptions={this.pageSizeOptions}
onSetPage={this.onSetPage}
/>
</Fragment>
@ -353,4 +348,12 @@ class Notifications extends Component {
}
}
Notifications.propType = {
getError: PropTypes.func.isRequired,
getNotifications: PropTypes.func.isRequired,
getSuccess: PropTypes.func.isRequired,
postError: PropTypes.func.isRequired,
postSuccess: PropTypes.func.isRequired,
};
export default Notifications;

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { t } from '@lingui/macro';
@ -126,4 +127,14 @@ class PageHeaderToolbar extends Component {
}
}
PageHeaderToolbar.propTypes = {
isAboutDisabled: PropTypes.bool,
onAboutClick: PropTypes.func.isRequired,
onLogoutClick: PropTypes.func.isRequired,
};
PageHeaderToolbar.defaultProps = {
isAboutDisabled: false,
};
export default PageHeaderToolbar;

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { I18n } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import {
@ -224,4 +225,19 @@ class Pagination extends Component {
}
}
Pagination.propTypes = {
count: PropTypes.number,
onSetPage: PropTypes.func.isRequired,
page: PropTypes.number.isRequired,
pageCount: PropTypes.number,
pageSizeOptions: PropTypes.arrayOf(PropTypes.number),
page_size: PropTypes.number.isRequired,
};
Pagination.defaultProps = {
count: null,
pageCount: null,
pageSizeOptions: [5, 10, 25, 50],
};
export default Pagination;

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Chip
} from '@patternfly/react-core';
@ -70,4 +71,16 @@ class SelectedList extends Component {
}
}
SelectedList.propTypes = {
label: PropTypes.string,
onRemove: PropTypes.func.isRequired,
selected: PropTypes.arrayOf(PropTypes.object).isRequired,
showOverflowAfter: PropTypes.number,
};
SelectedList.defaultProps = {
label: 'Selected',
showOverflowAfter: 5,
};
export default SelectedList;

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import './tabs.scss';
@ -15,4 +16,18 @@ const Tab = ({ children, link, replace }) => (
</li>
);
Tab.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
link: PropTypes.string,
replace: PropTypes.bool,
};
Tab.defaultProps = {
link: null,
replace: false,
};
export default Tab;

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Button } from '@patternfly/react-core';
import { TimesIcon } from '@patternfly/react-icons';
@ -35,4 +36,21 @@ const Tabs = ({ children, labelText, closeButton }) => (
</div>
);
Tabs.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
labelText: PropTypes.string,
closeButton: PropTypes.shape({
text: PropTypes.string,
link: PropTypes.string,
}),
};
Tabs.defaultProps = {
labelText: null,
closeButton: null,
};
export default Tabs;

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
class Tooltip extends React.Component {
transforms = {
@ -74,4 +75,14 @@ class Tooltip extends React.Component {
}
}
Tooltip.propTypes = {
children: PropTypes.element.isRequired,
message: PropTypes.string.isRequired,
position: PropTypes.string,
};
Tooltip.defaultProps = {
position: 'top',
};
export default Tooltip;

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
@ -58,4 +59,12 @@ class TowerLogo extends Component {
}
}
TowerLogo.propTypes = {
linkTo: PropTypes.string,
};
TowerLogo.defaultProps = {
linkTo: null,
};
export default withRouter(TowerLogo);

View File

@ -137,16 +137,14 @@ class OrganizationAdd extends React.Component {
</FormGroup>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
<FormGroup label="Ansible Environment" fieldId="add-org-form-custom-virtualenv">
<AnsibleSelect
label="Ansible Environment"
name="custom_virtualenv"
value={custom_virtualenv}
onChange={this.onFieldChange}
data={custom_virtualenvs}
defaultSelected={defaultEnv}
/>
</FormGroup>
<AnsibleSelect
label="Ansible Environment"
name="custom_virtualenv"
value={custom_virtualenv}
onChange={this.onFieldChange}
data={custom_virtualenvs}
defaultSelected={defaultEnv}
/>
)}
</ConfigContext.Consumer>
</Gallery>

View File

@ -39,8 +39,6 @@ class OrganizationsList extends Component {
order_by: 'name',
};
pageSizeOptions = [5, 10, 25, 50];
constructor (props) {
super(props);
@ -207,7 +205,6 @@ class OrganizationsList extends Component {
selected,
} = this.state;
const { match } = this.props;
return (
<PageSection variant={medium}>
<Card>
@ -258,7 +255,6 @@ class OrganizationsList extends Component {
page={page}
pageCount={pageCount}
page_size={page_size}
pageSizeOptions={this.pageSizeOptions}
onSetPage={this.onSetPage}
/>
{ loading ? <div>loading...</div> : '' }