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, searchColumns: SearchColumns.isRequired,
searchableKeys: PropTypes.arrayOf(PropTypes.string), searchableKeys: PropTypes.arrayOf(PropTypes.string),
relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string), relatedSearchableKeys: PropTypes.arrayOf(PropTypes.string),
sortColumns: SortColumns.isRequired, sortColumns: SortColumns,
renderToolbar: PropTypes.func, renderToolbar: PropTypes.func,
}; };
ListHeader.defaultProps = { ListHeader.defaultProps = {
renderToolbar: props => <DataListToolbar {...props} />, renderToolbar: props => <DataListToolbar {...props} />,
searchableKeys: [], searchableKeys: [],
sortColumns: null,
relatedSearchableKeys: [], 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 numActions = children.length || 1;
const width = numActions * 40; const width = numActions * 40;
return ( return (
@@ -25,6 +25,7 @@ export default function ActionsTd({ children }) {
text-align: right; text-align: right;
--pf-c-table--cell--Width: ${width}px; --pf-c-table--cell--Width: ${width}px;
`} `}
{...props}
> >
<ActionsGrid numActions={numActions}> <ActionsGrid numActions={numActions}>
{React.Children.map(children, (child, i) => {React.Children.map(children, (child, i) =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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