update tests for PaginatedTable lists

This commit is contained in:
Keith Grant 2020-12-08 16:35:42 -08:00
parent 9da636e294
commit 204af9ec91
11 changed files with 259 additions and 219 deletions

View File

@ -147,13 +147,14 @@ ListHeader.propTypes = {
searchColumns: SearchColumns.isRequired,
searchableKeys: PropTypes.arrayOf(PropTypes.string),
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
sortColumns: SortColumns.isRequired,
sortColumns: SortColumns,
renderToolbar: PropTypes.func,
};
ListHeader.defaultProps = {
renderToolbar: props => <DataListToolbar {...props} />,
searchableKeys: [],
sortColumns: null,
relatedSearchableKeys: [],
};

View File

@ -16,7 +16,7 @@ const ActionsGrid = styled.div`
}}
`;
export default function ActionsTd({ children }) {
export default function ActionsTd({ children, ...props }) {
const numActions = children.length || 1;
const width = numActions * 40;
return (
@ -25,6 +25,7 @@ export default function ActionsTd({ children }) {
text-align: right;
--pf-c-table--cell--Width: ${width}px;
`}
{...props}
>
<ActionsGrid numActions={numActions}>
{React.Children.map(children, (child, i) =>

View File

@ -36,15 +36,9 @@ function PaginatedTable({
showPageSizeOptions,
i18n,
renderToolbar,
// onRowClick,
}) {
const history = useHistory();
// const handleListItemSelect = (id = 0) => {
// const match = items.find(item => item.id === Number(id));
// onRowClick(match);
// };
const pushHistoryState = params => {
const { pathname } = history.location;
const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
@ -174,7 +168,6 @@ PaginatedTable.propTypes = {
renderToolbar: PropTypes.func,
hasContentLoading: PropTypes.bool,
contentError: PropTypes.shape(),
// onRowClick: PropTypes.func,
};
PaginatedTable.defaultProps = {
@ -187,7 +180,6 @@ PaginatedTable.defaultProps = {
showPageSizeOptions: true,
renderRow: item => <PaginatedTableRow key={item.id} item={item} />,
renderToolbar: props => <DataListToolbar {...props} />,
// onRowClick: () => null,
};
export { PaginatedTable as _PaginatedTable };

View File

@ -8,6 +8,7 @@ import {
SyncAltIcon,
ExclamationTriangleIcon,
ClockIcon,
MinusCircleIcon,
} from '@patternfly/react-icons';
import styled, { keyframes } from 'styled-components';
@ -32,6 +33,7 @@ const colors = {
running: 'blue',
pending: 'blue',
waiting: 'grey',
disabled: 'grey',
canceled: 'orange',
};
const icons = {
@ -42,6 +44,7 @@ const icons = {
running: RunningIcon,
pending: ClockIcon,
waiting: ClockIcon,
disabled: MinusCircleIcon,
canceled: ExclamationTriangleIcon,
};
@ -66,6 +69,7 @@ StatusLabel.propTypes = {
'running',
'pending',
'waiting',
'disabled',
'canceled',
]).isRequired,
};

View File

@ -162,7 +162,6 @@ function InventoryList({ i18n }) {
itemCount={itemCount}
pluralizedItemName={i18n._(t`Inventories`)}
qsConfig={QS_CONFIG}
onRowClick={handleSelect}
toolbarSearchColumns={[
{
name: i18n._(t`Name`),

View File

@ -56,15 +56,16 @@ function InventoryListItem({
}
return (
<Tr id={inventory.id}>
<Tr id={inventory.id} aria-labelledby={labelId}>
<Td
select={{
rowIndex,
isSelected,
onSelect,
}}
dataLabel={i18n._(t`Selected`)}
/>
<Td id={labelId}>
<Td id={labelId} dataLabel={i18n._(t`Name`)}>
{inventory.pending_deletion ? (
<b>{inventory.name}</b>
) : (
@ -73,30 +74,32 @@ function InventoryListItem({
</Link>
)}
</Td>
<Td>
<Td dataLabel={i18n._(t`Status`)}>
{inventory.kind !== 'smart' && <StatusLabel status={syncStatus} />}
</Td>
<Td>
<Td dataLabel={i18n._(t`Type`)}>
{inventory.kind === 'smart'
? i18n._(t`Smart Inventory`)
: i18n._(t`Inventory`)}
</Td>
<Td key="organization">
<Td key="organization" dataLabel={i18n._(t`Organization`)}>
<Link
to={`/organizations/${inventory.summary_fields.organization.id}/details`}
>
{inventory.summary_fields.organization.name}
</Link>
</Td>
<Td>{inventory.total_groups}</Td>
<Td>{inventory.total_hosts}</Td>
<Td>{inventory.total_inventory_sources}</Td>
<Td dataLabel={i18n._(t`Groups`)}>{inventory.total_groups}</Td>
<Td dataLabel={i18n._(t`Hosts`)}>{inventory.total_hosts}</Td>
<Td dataLabel={i18n._(t`Sources`)}>
{inventory.total_inventory_sources}
</Td>
{inventory.pending_deletion ? (
<Td>
<Td dataLabel={i18n._(t`Groups`)}>
<Label color="red">{i18n._(t`Pending delete`)}</Label>
</Td>
) : (
<ActionsTd>
<ActionsTd dataLabel={i18n._(t`Actions`)}>
<ActionItem
visible={inventory.summary_fields.user_capabilities.edit}
tooltip={i18n._(t`Edit Inventory`)}

View File

@ -9,145 +9,167 @@ jest.mock('../../../api/models/Inventories');
describe('<InventoryListItem />', () => {
test('initially renders succesfully', () => {
mountWithContexts(
<InventoryListItem
inventory={{
id: 1,
name: 'Inventory',
summary_fields: {
organization: {
<table>
<tbody>
<InventoryListItem
inventory={{
id: 1,
name: 'Default',
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
name: 'Inventory',
summary_fields: {
organization: {
id: 1,
name: 'Default',
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
});
test('should render prompt list item data', () => {
const wrapper = mountWithContexts(
<InventoryListItem
inventory={{
id: 1,
name: 'Inventory',
kind: '',
summary_fields: {
organization: {
<table>
<tbody>
<InventoryListItem
inventory={{
id: 1,
name: 'Default',
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
name: 'Inventory',
kind: '',
summary_fields: {
organization: {
id: 1,
name: 'Default',
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
expect(wrapper.find('SyncStatusIndicator').length).toBe(1);
expect(wrapper.find('StatusLabel').length).toBe(1);
expect(
wrapper
.find('DataListCell')
.find('Td')
.at(1)
.text()
).toBe('Inventory');
expect(
wrapper
.find('DataListCell')
.find('Td')
.at(2)
.text()
).toBe('Disabled');
expect(
wrapper
.find('Td')
.at(3)
.text()
).toBe('Inventory');
expect(
wrapper
.find('DataListCell')
.at(3)
.text()
).toBe('OrganizationDefault');
expect(
wrapper
.find('DataListCell')
.find('Td')
.at(4)
.text()
).toBe('GroupsHostsSources');
).toBe('Default');
});
test('edit button shown to users with edit capabilities', () => {
const wrapper = mountWithContexts(
<InventoryListItem
inventory={{
id: 1,
name: 'Inventory',
summary_fields: {
organization: {
<table>
<tbody>
<InventoryListItem
inventory={{
id: 1,
name: 'Default',
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
name: 'Inventory',
summary_fields: {
organization: {
id: 1,
name: 'Default',
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
});
test('edit button hidden from users without edit capabilities', () => {
const wrapper = mountWithContexts(
<InventoryListItem
inventory={{
id: 1,
name: 'Inventory',
summary_fields: {
organization: {
<table>
<tbody>
<InventoryListItem
inventory={{
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
name: 'Inventory',
summary_fields: {
organization: {
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
});
test('should call api to copy inventory', async () => {
InventoriesAPI.copy.mockResolvedValue();
const wrapper = mountWithContexts(
<InventoryListItem
inventory={{
id: 1,
name: 'Inventory',
summary_fields: {
organization: {
<table>
<tbody>
<InventoryListItem
inventory={{
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
copy: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
name: 'Inventory',
summary_fields: {
organization: {
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
copy: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
await act(async () =>
@ -161,25 +183,29 @@ describe('<InventoryListItem />', () => {
InventoriesAPI.copy.mockRejectedValue(new Error());
const wrapper = mountWithContexts(
<InventoryListItem
inventory={{
id: 1,
name: 'Inventory',
summary_fields: {
organization: {
<table>
<tbody>
<InventoryListItem
inventory={{
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
copy: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
name: 'Inventory',
summary_fields: {
organization: {
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
copy: true,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
await act(async () =>
wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
@ -191,25 +217,29 @@ describe('<InventoryListItem />', () => {
test('should not render copy button', async () => {
const wrapper = mountWithContexts(
<InventoryListItem
inventory={{
id: 1,
name: 'Inventory',
summary_fields: {
organization: {
<table>
<tbody>
<InventoryListItem
inventory={{
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
copy: false,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
name: 'Inventory',
summary_fields: {
organization: {
id: 1,
name: 'Default',
},
user_capabilities: {
edit: false,
copy: false,
},
},
}}
detailUrl="/inventories/inventory/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
expect(wrapper.find('CopyButton').length).toBe(0);
});

View File

@ -122,14 +122,12 @@ function OrganizationsList({ i18n }) {
<PageSection>
<Card>
<PaginatedTable
// TODO: audit if any of these props are no longer in use
contentError={contentError}
hasContentLoading={hasContentLoading}
items={organizations}
itemCount={organizationCount}
pluralizedItemName={i18n._(t`Organizations`)}
qsConfig={QS_CONFIG}
onRowClick={handleSelect}
toolbarSearchColumns={[
{
name: i18n._(t`Name`),

View File

@ -103,7 +103,7 @@ describe('<OrganizationsList />', () => {
});
test('Item appears selected after selecting it', async () => {
const itemCheckboxInput = 'input#select-organization-1';
const itemCheckboxInput = 'tr#org-row-1 input[type="checkbox"]';
await act(async () => {
wrapper = mountWithContexts(<OrganizationsList />);
});
@ -115,7 +115,6 @@ describe('<OrganizationsList />', () => {
await act(async () => {
wrapper
.find(itemCheckboxInput)
.closest('DataListCheck')
.props()
.onChange();
});
@ -128,9 +127,9 @@ describe('<OrganizationsList />', () => {
test('All items appear selected after select-all and unselected after unselect-all', async () => {
const itemCheckboxInputs = [
'input#select-organization-1',
'input#select-organization-2',
'input#select-organization-3',
'tr#org-row-1 input[type="checkbox"]',
'tr#org-row-2 input[type="checkbox"]',
'tr#org-row-3 input[type="checkbox"]',
];
await act(async () => {
wrapper = mountWithContexts(<OrganizationsList />);
@ -227,7 +226,7 @@ describe('<OrganizationsList />', () => {
});
test('Error dialog shown for failed deletion', async () => {
const itemCheckboxInput = 'input#select-organization-1';
const itemCheckboxInput = 'tr#org-row-1 input[type="checkbox"]';
OrganizationsAPI.destroy.mockRejectedValue(
new Error({
response: {
@ -250,7 +249,6 @@ describe('<OrganizationsList />', () => {
await act(async () => {
wrapper
.find(itemCheckboxInput)
.closest('DataListCheck')
.props()
.onChange();
});

View File

@ -20,7 +20,7 @@ function OrganizationListItem({
}) {
const labelId = `check-action-${organization.id}`;
return (
<Tr id={`${organization.id}`}>
<Tr id={`org-row-${organization.id}`}>
<Td
select={{
rowIndex,

View File

@ -11,77 +11,91 @@ describe('<OrganizationListItem />', () => {
mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<OrganizationListItem
organization={{
id: 1,
name: 'Org',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<OrganizationListItem
organization={{
id: 1,
name: 'Org',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);
});
test('edit button shown to users with edit capabilities', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<OrganizationListItem
organization={{
id: 1,
name: 'Org',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<OrganizationListItem
organization={{
id: 1,
name: 'Org',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
},
user_capabilities: {
edit: true,
},
},
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
});
test('edit button hidden from users without edit capabilities', () => {
const wrapper = mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<OrganizationListItem
organization={{
id: 1,
name: 'Org',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
},
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>
<table>
<tbody>
<OrganizationListItem
organization={{
id: 1,
name: 'Org',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
},
user_capabilities: {
edit: false,
},
},
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
</MemoryRouter>
</I18nProvider>
);