diff --git a/awx/ui/src/components/About/About.test.js b/awx/ui/src/components/About/About.test.js
index f3dede495..3511d1caf 100644
--- a/awx/ui/src/components/About/About.test.js
+++ b/awx/ui/src/components/About/About.test.js
@@ -1,5 +1,6 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import About from './About';
jest.mock('../../hooks/useBrandName', () => ({
@@ -8,14 +9,24 @@ jest.mock('../../hooks/useBrandName', () => ({
}));
describe(' ', () => {
- test('should render AboutModal', () => {
+ test('should render AboutModal with product name and version', () => {
const onClose = jest.fn();
- const wrapper = mountWithContexts( );
+ renderWithContexts( );
- const modal = wrapper.find('AboutModal');
- expect(modal).toHaveLength(1);
- expect(modal.prop('onClose')).toEqual(onClose);
- expect(modal.prop('productName')).toEqual('AWX');
- expect(modal.prop('isOpen')).toEqual(true);
+ // AboutModal renders into a body portal; the product name surfaces as the
+ // dialog's accessible name.
+ const dialog = screen.getByRole('dialog');
+ expect(dialog).toBeInTheDocument();
+ expect(screen.getByText('AWX')).toBeInTheDocument();
+
+ // The version is rendered inside the speech-bubble
.
+ const pre = dialog.querySelector('pre');
+ expect(pre.textContent).toContain('AWX 1.2.3');
+ });
+
+ test('should not render when isOpen is false', () => {
+ const onClose = jest.fn();
+ renderWithContexts( );
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/AddDropDownButton/AddDropDownButton.test.js b/awx/ui/src/components/AddDropDownButton/AddDropDownButton.test.js
index d1e16aaa2..ea61bcc7b 100644
--- a/awx/ui/src/components/AddDropDownButton/AddDropDownButton.test.js
+++ b/awx/ui/src/components/AddDropDownButton/AddDropDownButton.test.js
@@ -1,6 +1,7 @@
import React from 'react';
+import { screen, waitFor } from '@testing-library/react';
import { DropdownItem } from '@patternfly/react-core';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import AddDropDownButton from './AddDropDownButton';
describe(' ', () => {
@@ -8,29 +9,34 @@ describe(' ', () => {
Add ,
Route ,
];
+
test('should be closed initially', () => {
- const wrapper = mountWithContexts(
-
- );
- expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(false);
+ renderWithContexts( );
+ expect(screen.queryByRole('menuitem')).not.toBeInTheDocument();
});
- test('should render two links', () => {
- const wrapper = mountWithContexts(
+ test('should render the dropdown items when opened', async () => {
+ const { user } = renderWithContexts(
);
- wrapper.find('button').simulate('click');
- expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(true);
- expect(wrapper.find('DropdownItem')).toHaveLength(dropdownItems.length);
+ await user.click(screen.getByRole('button', { name: 'Add' }));
+ await waitFor(() =>
+ expect(screen.getAllByRole('menuitem')).toHaveLength(dropdownItems.length)
+ );
});
- test('should close when button re-clicked', () => {
- const wrapper = mountWithContexts(
+ test('should close when button re-clicked', async () => {
+ const { user } = renderWithContexts(
);
- wrapper.find('button').simulate('click');
- expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(true);
- wrapper.find('button').simulate('click');
- expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(false);
+ const toggle = screen.getByRole('button', { name: 'Add' });
+ await user.click(toggle);
+ await waitFor(() =>
+ expect(screen.getAllByRole('menuitem')).toHaveLength(dropdownItems.length)
+ );
+ await user.click(toggle);
+ await waitFor(() =>
+ expect(screen.queryByRole('menuitem')).not.toBeInTheDocument()
+ );
});
});
diff --git a/awx/ui/src/components/AnsibleSelect/AnsibleSelect.test.js b/awx/ui/src/components/AnsibleSelect/AnsibleSelect.test.js
index f0e2416af..40bc2f402 100644
--- a/awx/ui/src/components/AnsibleSelect/AnsibleSelect.test.js
+++ b/awx/ui/src/components/AnsibleSelect/AnsibleSelect.test.js
@@ -1,5 +1,6 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import AnsibleSelect from './AnsibleSelect';
const mockData = [
@@ -16,9 +17,8 @@ const mockData = [
];
describe(' ', () => {
- const onChange = jest.fn();
- test('initially renders successfully', async () => {
- mountWithContexts(
+ test('initially renders successfully', () => {
+ renderWithContexts(
', () => {
data={mockData}
/>
);
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
});
- test('calls "onSelectChange" on dropdown select change', () => {
- const wrapper = mountWithContexts(
+ test('calls "onChange" on dropdown select change', async () => {
+ const onChange = jest.fn();
+ const { user } = renderWithContexts(
);
expect(onChange).not.toHaveBeenCalled();
- wrapper.find('select').simulate('change');
+ await user.selectOptions(
+ screen.getByRole('combobox'),
+ '/var/lib/awx/venv/ansible/'
+ );
expect(onChange).toHaveBeenCalled();
+ // onSelectChange forwards (event, value); the selected value is the option.
+ const [, value] = onChange.mock.calls[0];
+ expect(value).toEqual('/var/lib/awx/venv/ansible/');
});
test('Returns correct select options', () => {
- const wrapper = mountWithContexts(
+ renderWithContexts(
', () => {
/>
);
- expect(wrapper.find('FormSelect')).toHaveLength(1);
- expect(wrapper.find('FormSelectOption')).toHaveLength(2);
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
+ const options = screen.getAllByRole('option');
+ expect(options).toHaveLength(2);
+ expect(options.map((o) => o.textContent)).toEqual(['Baz', 'Default']);
});
});
diff --git a/awx/ui/src/components/AssociateModal/AssociateModal.test.js b/awx/ui/src/components/AssociateModal/AssociateModal.test.js
index e12e9e748..319e0de7f 100644
--- a/awx/ui/src/components/AssociateModal/AssociateModal.test.js
+++ b/awx/ui/src/components/AssociateModal/AssociateModal.test.js
@@ -1,23 +1,18 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
-
-import {
- mountWithContexts,
- waitForElement,
-} from '../../../testUtils/enzymeHelpers';
+import { screen, waitFor, within } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import AssociateModal from './AssociateModal';
import mockHosts from './data.hosts.json';
jest.mock('../../api');
describe(' ', () => {
- let wrapper;
let onClose;
let onAssociate;
let fetchRequest;
let optionsRequest;
- beforeEach(async () => {
+ beforeEach(() => {
onClose = jest.fn();
onAssociate = jest.fn().mockResolvedValue();
fetchRequest = jest.fn().mockReturnValue({ data: { ...mockHosts } });
@@ -30,55 +25,72 @@ describe(' ', () => {
related_search_fields: [],
},
});
- await act(async () => {
- wrapper = mountWithContexts(
-
- );
- });
- await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
});
afterEach(() => {
jest.clearAllMocks();
});
- test('should render successfully', () => {
- expect(wrapper.find('AssociateModal').length).toBe(1);
- });
+ async function setup() {
+ const result = renderWithContexts(
+
+ );
+ // wait for the loading state to clear and rows to render
+ await waitFor(() =>
+ expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
+ );
+ return result;
+ }
- test('should fetch and render list items', () => {
+ test('should fetch and render list items', async () => {
+ await setup();
expect(fetchRequest).toHaveBeenCalledTimes(1);
expect(optionsRequest).toHaveBeenCalledTimes(1);
- expect(wrapper.find('CheckboxListItem').length).toBe(3);
+ // three hosts in the mock fixture, each rendered as a selectable row
+ expect(screen.getAllByRole('checkbox')).toHaveLength(mockHosts.count);
});
- test('should update selected list chips when items are selected', () => {
- expect(wrapper.find('SelectedList Chip')).toHaveLength(0);
- act(() => {
- wrapper.find('CheckboxListItem').first().invoke('onSelect')();
- });
- wrapper.update();
- expect(wrapper.find('SelectedList Chip')).toHaveLength(1);
- wrapper.find('SelectedList Chip button').simulate('click');
- expect(wrapper.find('SelectedList Chip')).toHaveLength(0);
+ test('should update selected list chips when items are selected', async () => {
+ const { user } = await setup();
+ const dialog = screen.getByRole('dialog');
+ // no chips initially
+ expect(within(dialog).queryByText('Selected')).not.toBeInTheDocument();
+
+ const [firstCheckbox] = screen.getAllByRole('checkbox');
+ await user.click(firstCheckbox);
+
+ await waitFor(() =>
+ expect(within(dialog).getByText('Selected')).toBeInTheDocument()
+ );
+ // the selected host name appears as a chip
+ const firstName = mockHosts.results[0].name;
+ expect(screen.getAllByText(firstName).length).toBeGreaterThanOrEqual(1);
});
- test('save button should call onAssociate', () => {
- act(() => {
- wrapper.find('CheckboxListItem').first().invoke('onSelect')();
- });
- wrapper.find('button[aria-label="Save"]').simulate('click');
- expect(onAssociate).toHaveBeenCalledTimes(1);
+ test('save button should call onAssociate', async () => {
+ const { user } = await setup();
+ const [firstCheckbox] = screen.getAllByRole('checkbox');
+ await user.click(firstCheckbox);
+
+ const saveButton = screen.getByRole('button', { name: 'Save' });
+ await waitFor(() => expect(saveButton).toBeEnabled());
+ await user.click(saveButton);
+
+ await waitFor(() => expect(onAssociate).toHaveBeenCalledTimes(1));
+ expect(onAssociate).toHaveBeenCalledWith([
+ expect.objectContaining({ id: mockHosts.results[0].id }),
+ ]);
});
- test('cancel button should call onClose', () => {
- wrapper.find('button[aria-label="Cancel"]').simulate('click');
+ test('cancel button should call onClose', async () => {
+ const { user } = await setup();
+ await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onClose).toHaveBeenCalledTimes(1);
});
});
diff --git a/awx/ui/src/components/CheckboxListItem/CheckboxListItem.test.js b/awx/ui/src/components/CheckboxListItem/CheckboxListItem.test.js
index 426735def..247180953 100644
--- a/awx/ui/src/components/CheckboxListItem/CheckboxListItem.test.js
+++ b/awx/ui/src/components/CheckboxListItem/CheckboxListItem.test.js
@@ -1,11 +1,17 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen, fireEvent } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import CheckboxListItem from './CheckboxListItem';
+// CheckboxListItem renders a PF whose checkbox only renders
+// inside a full PF Table context; mounted standalone here the row content and
+// the Tr onClick selection behaviour are what we assert (matching the original
+// enzyme coverage, which never reached the checkbox input either).
+
describe('CheckboxListItem', () => {
test('renders the expected content', () => {
- const wrapper = mountWithContexts(
+ renderWithContexts(
);
- expect(wrapper).toHaveLength(1);
+ expect(screen.getByText('Buzz')).toBeInTheDocument();
+ expect(screen.getByRole('row')).toBeInTheDocument();
+ });
+
+ test('clicking an unselected row calls onSelect with the item id', async () => {
+ const onSelect = jest.fn();
+ const onDeselect = jest.fn();
+ renderWithContexts(
+
+ );
+ fireEvent.click(screen.getByRole('row'));
+ expect(onSelect).toHaveBeenCalledWith(7);
+ expect(onDeselect).not.toHaveBeenCalled();
+ });
+
+ test('clicking a selected row calls onDeselect with the item id', async () => {
+ const onSelect = jest.fn();
+ const onDeselect = jest.fn();
+ renderWithContexts(
+
+ );
+ fireEvent.click(screen.getByRole('row'));
+ expect(onDeselect).toHaveBeenCalledWith(7);
+ expect(onSelect).not.toHaveBeenCalled();
});
test('should render row actions', () => {
- const wrapper = mountWithContexts(
+ renderWithContexts(
{
onSelect={() => {}}
onDeselect={() => {}}
rowActions={[
- action_1
,
- action_2
,
+
+ action_1
+
,
+
+ action_2
+
,
]}
/>
);
- expect(
- wrapper
- .find('ActionsTd')
- .containsAllMatchingElements([
- action_1
,
- action_2
,
- ])
- ).toEqual(true);
+ expect(screen.getByText('action_1')).toBeInTheDocument();
+ expect(screen.getByText('action_2')).toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/ContentError/ContentError.test.js b/awx/ui/src/components/ContentError/ContentError.test.js
index 518f0ae8c..4fd648494 100644
--- a/awx/ui/src/components/ContentError/ContentError.test.js
+++ b/awx/ui/src/components/ContentError/ContentError.test.js
@@ -1,11 +1,12 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import ContentError from './ContentError';
describe('ContentError', () => {
- test('renders the expected content', () => {
- const wrapper = mountWithContexts(
+ test('renders the generic error content', () => {
+ renderWithContexts(
{
}
/>
);
- expect(wrapper).toHaveLength(1);
+ expect(screen.getByText('Something went wrong...')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'There was an error loading this content. Please reload the page.'
+ )
+ ).toBeInTheDocument();
+ });
+
+ test('renders Not Found content for a 404 response', () => {
+ const error = new Error('not found');
+ error.response = { status: 404, headers: {} };
+ renderWithContexts( );
+ expect(screen.getByText('Not Found')).toBeInTheDocument();
+ expect(
+ screen.getByText('The page you requested could not be found.')
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText('Something went wrong...')
+ ).not.toBeInTheDocument();
+ });
+
+ test('renders Not Found content when isNotFound is set', () => {
+ renderWithContexts( );
+ expect(screen.getByText('Not Found')).toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/CopyButton/CopyButton.test.js b/awx/ui/src/components/CopyButton/CopyButton.test.js
index d1926945c..e07f182f4 100644
--- a/awx/ui/src/components/CopyButton/CopyButton.test.js
+++ b/awx/ui/src/components/CopyButton/CopyButton.test.js
@@ -1,42 +1,34 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import CopyButton from './CopyButton';
jest.mock('../../api');
-let wrapper;
-
describe(' ', () => {
- test('should mount properly', async () => {
- await act(async () => {
- wrapper = mountWithContexts(
- {}}
- onCopyFinish={() => {}}
- copyItem={() => {}}
- errorMessage="Failed to copy template."
- />
- );
- });
- expect(wrapper.find('CopyButton').length).toBe(1);
+ test('should mount properly', () => {
+ renderWithContexts(
+ {}}
+ onCopyFinish={() => {}}
+ copyItem={() => {}}
+ errorMessage="Failed to copy template."
+ />
+ );
+ expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument();
});
- test('should call the correct function on button click', async () => {
+ test('should call copyItem on button click', async () => {
const copyItem = jest.fn();
- await act(async () => {
- wrapper = mountWithContexts(
- {}}
- onCopyFinish={() => {}}
- copyItem={copyItem}
- errorMessage="Failed to copy template."
- />
- );
- });
- await act(async () => {
- wrapper.find('button').simulate('click');
- });
+ const { user } = renderWithContexts(
+ {}}
+ onCopyFinish={() => {}}
+ copyItem={copyItem}
+ errorMessage="Failed to copy template."
+ />
+ );
+ await user.click(screen.getByRole('button', { name: 'Copy' }));
expect(copyItem).toHaveBeenCalledTimes(1);
});
});
diff --git a/awx/ui/src/components/CredentialChip/CredentialChip.test.js b/awx/ui/src/components/CredentialChip/CredentialChip.test.js
index df678d5d2..547cfe5d5 100644
--- a/awx/ui/src/components/CredentialChip/CredentialChip.test.js
+++ b/awx/ui/src/components/CredentialChip/CredentialChip.test.js
@@ -1,5 +1,6 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import CredentialChip from './CredentialChip';
describe('CredentialChip', () => {
@@ -10,10 +11,11 @@ describe('CredentialChip', () => {
name: 'foo',
};
- const wrapper = mountWithContexts(
-
+ renderWithContexts( );
+ expect(screen.getByText(/SSH:/)).toBeInTheDocument();
+ expect(screen.getByText('foo', { exact: false })).toHaveTextContent(
+ 'SSH: foo'
);
- expect(wrapper.find('CredentialChip').text()).toEqual('SSH: foo');
});
test('should render AWS kind', () => {
@@ -23,10 +25,10 @@ describe('CredentialChip', () => {
name: 'foo',
};
- const wrapper = mountWithContexts(
-
+ renderWithContexts( );
+ expect(screen.getByText('foo', { exact: false })).toHaveTextContent(
+ 'AWS: foo'
);
- expect(wrapper.find('CredentialChip').text()).toEqual('AWS: foo');
});
test('should render with "Cloud"', () => {
@@ -37,10 +39,10 @@ describe('CredentialChip', () => {
name: 'foo',
};
- const wrapper = mountWithContexts(
-
+ renderWithContexts( );
+ expect(screen.getByText('foo', { exact: false })).toHaveTextContent(
+ 'Cloud: foo'
);
- expect(wrapper.find('CredentialChip').text()).toEqual('Cloud: foo');
});
test('should render with other kind', () => {
@@ -50,9 +52,26 @@ describe('CredentialChip', () => {
name: 'foo',
};
- const wrapper = mountWithContexts(
-
+ renderWithContexts( );
+ expect(screen.getByText('foo', { exact: false })).toHaveTextContent(
+ 'Other: foo'
);
- expect(wrapper.find('CredentialChip').text()).toEqual('Other: foo');
+ });
+
+ test('should render a deletable chip with a close button by default', () => {
+ const credential = { id: 1, kind: 'ssh', name: 'foo' };
+ renderWithContexts(
+ {}} />
+ );
+ // PF Chip's close button is labelled by the chip text (aria-labelledby).
+ expect(
+ screen.getByRole('button', { name: /SSH: foo/ })
+ ).toBeInTheDocument();
+ });
+
+ test('should render a read-only chip without a close button', () => {
+ const credential = { id: 1, kind: 'ssh', name: 'foo' };
+ renderWithContexts( );
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/DataListToolbar/DataListToolbar.test.js b/awx/ui/src/components/DataListToolbar/DataListToolbar.test.js
index 5132ec81b..870f99292 100644
--- a/awx/ui/src/components/DataListToolbar/DataListToolbar.test.js
+++ b/awx/ui/src/components/DataListToolbar/DataListToolbar.test.js
@@ -1,13 +1,10 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { shallow } from 'enzyme';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen, within, fireEvent } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import DataListToolbar from './DataListToolbar';
import AddDropDownButton from '../AddDropDownButton/AddDropDownButton';
describe(' ', () => {
- let toolbar;
-
const QS_CONFIG = {
namespace: 'organization',
dateFields: ['modified', 'created'],
@@ -21,17 +18,17 @@ describe(' ', () => {
const onSelectAll = jest.fn();
const onExpandAll = jest.fn();
- test('it triggers the expected callbacks', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it triggers the expected callbacks', async () => {
const searchColumns = [
{ name: 'Name', key: 'name__icontains', isDefault: true },
];
const sortColumns = [{ name: 'Name', key: 'name' }];
- const search = 'button[aria-label="Search submit button"]';
- const searchTextInput = 'input[aria-label="Search text input"]';
- const selectAll = 'input[aria-label="Select all"]';
- const sort = 'button[aria-label="Sort"]';
- toolbar = mountWithContexts(
+ const { user } = renderWithContexts(
', () => {
/>
);
- toolbar.find(sort).simulate('click');
- toolbar.find(selectAll).simulate('change', { target: { checked: false } });
-
- expect(onSelectAll).toHaveBeenCalledTimes(1);
+ await user.click(screen.getByRole('button', { name: 'Sort' }));
expect(onSort).toHaveBeenCalledTimes(1);
expect(onSort).toHaveBeenCalledWith('name', 'descending');
+ await user.click(screen.getByRole('checkbox', { name: 'Select all' }));
expect(onSelectAll).toHaveBeenCalledTimes(1);
- expect(onSelectAll.mock.calls[0][0]).toBe(false);
-
- toolbar.find(searchTextInput).instance().value = 'test-321';
- toolbar.find(searchTextInput).simulate('change');
- toolbar.find(search).simulate('click');
+ // starting unchecked, clicking toggles to checked -> PF Checkbox passes true
+ expect(onSelectAll.mock.calls[0][0]).toBe(true);
+ const input = screen.getByLabelText('Search text input');
+ await user.type(input, 'test-321');
+ await user.click(
+ screen.getByRole('button', { name: 'Search submit button' })
+ );
expect(onSearch).toHaveBeenCalledTimes(1);
expect(onSearch).toHaveBeenCalledWith('name__icontains', 'test-321');
});
- test('dropdown items sortable/searchable columns work', () => {
- const sortDropdownToggleSelector = 'button[id="awx-sort"]';
- const searchDropdownToggleSelector =
- 'Select[aria-label="Simple key select"] SelectToggle';
- const sortDropdownMenuItems =
- 'DropdownMenu > ul[aria-labelledby="awx-sort"]';
- const searchDropdownMenuItems =
- 'Select[aria-label="Simple key select"] SelectOption';
-
- const NEW_QS_CONFIG = {
- namespace: 'organization',
- dateFields: ['modified', 'created'],
- defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
- integerFields: ['page', 'page_size'],
- };
-
- const searchColumns = [
- { name: 'Foo', key: 'foo', isDefault: true },
- { name: 'Bar', key: 'bar' },
- ];
- const sortColumns = [
- { name: 'Foo', key: 'foo' },
- { name: 'Bar', key: 'bar' },
- { name: 'Bakery', key: 'Bakery' },
- ];
-
- toolbar = mountWithContexts(
- {}}
- />
- );
- const sortDropdownToggle = toolbar.find(sortDropdownToggleSelector);
- expect(sortDropdownToggle.length).toBe(1);
- sortDropdownToggle.simulate('click');
- toolbar.update();
- const sortDropdownItems = toolbar.find(sortDropdownMenuItems).children();
- expect(sortDropdownItems.length).toBe(2);
- let searchDropdownToggle = toolbar.find(searchDropdownToggleSelector);
- expect(searchDropdownToggle.length).toBe(1);
- searchDropdownToggle.simulate('click');
- toolbar.update();
- let searchDropdownItems = toolbar.find(searchDropdownMenuItems).children();
- expect(searchDropdownItems.length).toBe(2);
- const mockedSortEvent = { target: { innerText: 'Bar' } };
- searchDropdownItems.at(0).simulate('click', mockedSortEvent);
- toolbar = mountWithContexts(
+ test('should reflect isAllSelected on the select-all checkbox', () => {
+ const searchColumns = [{ name: 'Name', key: 'name', isDefault: true }];
+ const sortColumns = [{ name: 'Name', key: 'name' }];
+ renderWithContexts(
{}}
+ onSelectAll={onSelectAll}
+ showSelectAll
/>
);
- toolbar.update();
-
- const sortDropdownToggleDescending = toolbar.find(
- sortDropdownToggleSelector
- );
- expect(sortDropdownToggleDescending.length).toBe(1);
- sortDropdownToggleDescending.simulate('click');
- toolbar.update();
-
- const sortDropdownItemsDescending = toolbar
- .find(sortDropdownMenuItems)
- .children();
- expect(sortDropdownItemsDescending.length).toBe(2);
- sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown
-
- const mockedSortEventDescending = { target: { innerText: 'Bar' } };
- sortDropdownItems.at(0).simulate('click', mockedSortEventDescending);
- toolbar.update();
-
- searchDropdownToggle = toolbar.find(searchDropdownToggleSelector);
- expect(searchDropdownToggle.length).toBe(1);
- searchDropdownToggle.simulate('click');
- toolbar.update();
-
- searchDropdownItems = toolbar.find(searchDropdownMenuItems).children();
- expect(searchDropdownItems.length).toBe(2);
-
- const mockedSearchEvent = { target: { innerText: 'Bar' } };
- searchDropdownItems.at(0).simulate('click', mockedSearchEvent);
+ expect(screen.getByRole('checkbox', { name: 'Select all' })).toBeChecked();
});
- test('should render sort icon', () => {
- const qsConfig = {
- namespace: 'organization',
- dateFields: ['modified', 'created'],
- defaultParams: { page: 1, page_size: 5, order_by: 'name' },
- integerFields: ['page', 'page_size', 'id'],
- };
+ test('should render the sort control', () => {
const sortColumns = [{ name: 'Name', key: 'name' }];
-
- const wrapper = mountWithContexts(
+ renderWithContexts(
);
-
- const sort = wrapper.find('Sort');
- expect(sort.prop('qsConfig')).toEqual(qsConfig);
- expect(sort.prop('columns')).toEqual(sortColumns);
- expect(sort.prop('onSort')).toEqual(onSort);
+ expect(screen.getByRole('button', { name: 'Sort' })).toBeInTheDocument();
});
test('should render additionalControls', () => {
const searchColumns = [{ name: 'Name', key: 'name', isDefault: true }];
const sortColumns = [{ name: 'Name', key: 'name' }];
- toolbar = mountWithContexts(
+ renderWithContexts(
', () => {
/>
);
- const button = toolbar.find('#test');
- expect(button).toHaveLength(1);
- expect(button.text()).toEqual('click');
+ expect(screen.getByRole('button', { name: 'click' })).toBeInTheDocument();
});
- test('it triggers the expected callbacks', () => {
- const searchColumns = [{ name: 'Name', key: 'name', isDefault: true }];
- const sortColumns = [{ name: 'Name', key: 'name' }];
- toolbar = mountWithContexts(
-
- );
- const checkbox = toolbar.find('Checkbox');
- expect(checkbox.prop('isChecked')).toBe(true);
- });
-
- test('always adds advanced item to search column array', () => {
+ test('always adds advanced item to the search column dropdown', async () => {
const searchColumns = [{ name: 'Name', key: 'name', isDefault: true }];
const sortColumns = [{ name: 'Name', key: 'name' }];
- toolbar = mountWithContexts(
+ const { user } = renderWithContexts(
', () => {
onReplaceSearch={onReplaceSearch}
onSort={onSort}
onSelectAll={onSelectAll}
- additionalControls={[
-
- click
- ,
- ]}
/>
);
- const search = toolbar.find('Search');
+ // open the simple-key select and assert the injected "Advanced" option
+ await user.click(screen.getByRole('button', { name: 'Options menu' }));
expect(
- search.prop('columns').filter((col) => col.key === 'advanced').length
- ).toBe(1);
+ screen.getByRole('option', { name: 'Advanced' })
+ ).toBeInTheDocument();
});
- test('should properly render toolbar buttons when in advanced search mode', async () => {
+ test('should render the kebab and its items when in advanced search mode', async () => {
const searchColumns = [{ name: 'Name', key: 'name', isDefault: true }];
const sortColumns = [{ name: 'Name', key: 'name' }];
- const newToolbar = mountWithContexts(
+ const { user } = renderWithContexts(
', () => {
onSelectAll={onSelectAll}
additionalControls={[
- Add Contaner
+ Add Container
,
Add Instance Group
@@ -277,15 +168,22 @@ describe('
', () => {
]}
/>
);
- act(() => newToolbar.find('Search').prop('onShowAdvancedSearch')(true));
- newToolbar.update();
- expect(newToolbar.find('KebabToggle').length).toBe(1);
- act(() => newToolbar.find('KebabToggle').prop('onToggle')(true));
- newToolbar.update();
- expect(newToolbar.find('div[aria-label="add container"]').length).toBe(1);
- expect(newToolbar.find('div[aria-label="add instance group"]').length).toBe(
- 1
- );
+
+ // enter advanced search mode via the simple-key select -> "Advanced".
+ // Search.handleDropdownSelect matches the option by event.target.innerText,
+ // which jsdom does not populate from layout, so set it explicitly before
+ // dispatching the click (DOM equivalent of the option selection).
+ await user.click(screen.getByRole('button', { name: 'Options menu' }));
+ const advancedOption = screen.getByRole('option', { name: 'Advanced' });
+ advancedOption.innerText = 'Advanced';
+ fireEvent.click(advancedOption);
+
+ // a kebab toggle appears in advanced search mode
+ const kebab = await screen.findByRole('button', { name: 'Actions' });
+ await user.click(kebab);
+
+ expect(screen.getByLabelText('add container')).toBeInTheDocument();
+ expect(screen.getByLabelText('add instance group')).toBeInTheDocument();
});
test('should handle expanded rows', async () => {
@@ -294,7 +192,7 @@ describe('
', () => {
];
const sortColumns = [{ name: 'Name', key: 'name' }];
- const newtoolbar = mountWithContexts(
+ const { user } = renderWithContexts(
', () => {
onSort={onSort}
onSelectAll={onSelectAll}
showSelectAll
- showExpandAll
isAllExpanded={false}
onExpandAll={onExpandAll}
/>
);
- await act(async () =>
- newtoolbar.find('Button[aria-label="Expand all rows"]').prop('onClick')()
+
+ await user.click(
+ screen.getByRole('button', { name: 'Expand all rows' })
);
- expect(newtoolbar.find('AngleRightIcon')).toHaveLength(1);
- expect(newtoolbar.find('AngleDownIcon')).toHaveLength(0);
expect(onExpandAll).toHaveBeenCalledWith(true);
});
- test('should render angle down icon', async () => {
+ test('should render the expand/collapse all toggle reflecting isAllExpanded', () => {
const searchColumns = [
{ name: 'Name', key: 'name__icontains', isDefault: true },
];
const sortColumns = [{ name: 'Name', key: 'name' }];
- const newtoolbar = mountWithContexts(
+ renderWithContexts(
', () => {
onSort={onSort}
onSelectAll={onSelectAll}
showSelectAll
- showExpandAll
isAllExpanded
onExpandAll={onExpandAll}
/>
);
- expect(newtoolbar.find('AngleDownIcon')).toHaveLength(1);
- expect(newtoolbar.find('AngleRightIcon')).toHaveLength(0);
+ const expandButton = screen.getByRole('button', {
+ name: 'Expand all rows',
+ });
+ expect(
+ within(expandButton).getByLabelText('Is expanded')
+ ).toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/DisassociateButton/DisassociateButton.test.js b/awx/ui/src/components/DisassociateButton/DisassociateButton.test.js
index 6a1cf5b04..a19c4769a 100644
--- a/awx/ui/src/components/DisassociateButton/DisassociateButton.test.js
+++ b/awx/ui/src/components/DisassociateButton/DisassociateButton.test.js
@@ -1,144 +1,141 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen, waitFor, within } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import DisassociateButton from './DisassociateButton';
describe('
', () => {
describe('User has disassociate permissions', () => {
- let wrapper;
- const handleDisassociate = jest.fn();
const mockHosts = [
{
id: 1,
name: 'foo',
- summary_fields: {
- user_capabilities: {
- delete: true,
- },
- },
+ summary_fields: { user_capabilities: { delete: true } },
},
{
id: 2,
name: 'bar',
- summary_fields: {
- user_capabilities: {
- delete: true,
- },
- },
+ summary_fields: { user_capabilities: { delete: true } },
},
];
- beforeAll(() => {
- wrapper = mountWithContexts(
+ test('should render an enabled disassociate button', () => {
+ renderWithContexts(
{}}
itemsToDisassociate={mockHosts}
modalNote="custom note"
modalTitle="custom title"
/>
);
+ const button = screen.getByRole('button', { name: 'Disassociate' });
+ expect(button).toBeEnabled();
});
- afterAll(() => {
- jest.clearAllMocks();
- });
+ test('should open confirmation modal and render expected content', async () => {
+ const { user } = renderWithContexts(
+ {}}
+ itemsToDisassociate={mockHosts}
+ modalNote="custom note"
+ modalTitle="custom title"
+ />
+ );
- test('should render button', () => {
- expect(wrapper.find('button')).toHaveLength(1);
- expect(wrapper.find('button').text()).toEqual('Disassociate');
- });
+ await user.click(screen.getByRole('button', { name: 'Disassociate' }));
- test('should open confirmation modal', () => {
- wrapper.find('button').simulate('click');
- expect(wrapper.find('AlertModal')).toHaveLength(1);
+ const dialog = await screen.findByRole('dialog');
+ expect(within(dialog).getByText('custom title')).toBeInTheDocument();
+ expect(within(dialog).getByText('custom note')).toBeInTheDocument();
+ expect(
+ within(dialog).getByText('This action will disassociate the following:')
+ ).toBeInTheDocument();
+ expect(within(dialog).getByText('foo')).toBeInTheDocument();
+ expect(within(dialog).getByText('bar')).toBeInTheDocument();
});
- test('cancel button should close confirmation modal', () => {
- expect(wrapper.find('AlertModal')).toHaveLength(1);
- wrapper.find('button[aria-label="Cancel"]').simulate('click');
- expect(wrapper.find('AlertModal')).toHaveLength(0);
- });
+ test('cancel button should close confirmation modal', async () => {
+ const { user } = renderWithContexts(
+ {}}
+ itemsToDisassociate={mockHosts}
+ modalTitle="custom title"
+ />
+ );
- test('should render expected modal content', () => {
- wrapper.find('button').simulate('click');
- expect(
- wrapper
- .find('AlertModal')
- .containsMatchingElement(custom note
)
- ).toEqual(true);
- expect(
- wrapper
- .find('AlertModal')
- .containsMatchingElement(
- This action will disassociate the following:
- )
- ).toEqual(true);
- expect(wrapper.find('Title').text()).toEqual('custom title');
- wrapper.find('button[aria-label="Close"]').simulate('click');
+ await user.click(screen.getByRole('button', { name: 'Disassociate' }));
+ const dialog = await screen.findByRole('dialog');
+ await user.click(
+ within(dialog).getByRole('button', { name: 'Cancel' })
+ );
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
});
- test('disassociate button should call handleDisassociate on click', () => {
- wrapper.find('button').simulate('click');
+ test('confirm button should call onDisassociate and close the modal', async () => {
+ const handleDisassociate = jest.fn();
+ const { user } = renderWithContexts(
+
+ );
+
+ await user.click(screen.getByRole('button', { name: 'Disassociate' }));
+ const dialog = await screen.findByRole('dialog');
expect(handleDisassociate).toHaveBeenCalledTimes(0);
- wrapper
- .find('button[aria-label="confirm disassociate"]')
- .simulate('click');
+
+ await user.click(
+ within(dialog).getByRole('button', { name: 'confirm disassociate' })
+ );
expect(handleDisassociate).toHaveBeenCalledTimes(1);
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
});
});
describe('User does not have disassociate permissions', () => {
- const readOnlyHost = [
- {
- id: 1,
- name: 'foo',
- summary_fields: {
- user_capabilities: {
- delete: false,
- },
- },
- },
- ];
-
test('should disable button when no delete permissions', () => {
- const wrapper = mountWithContexts(
+ renderWithContexts(
{}}
- itemsToDelete={readOnlyHost}
+ itemsToDisassociate={[
+ {
+ id: 1,
+ name: 'foo',
+ summary_fields: { user_capabilities: { delete: false } },
+ },
+ ]}
/>
);
- expect(wrapper.find('button[disabled]')).toHaveLength(1);
+ expect(screen.getByRole('button', { name: 'Disassociate' })).toBeDisabled();
});
test('should disable button for control instance', () => {
- const wrapper = mountWithContexts(
+ renderWithContexts(
{}}
- itemsToDelete={[
- {
- id: 1,
- hostname: 'awx',
- node_type: 'control',
- },
+ itemsToDisassociate={[
+ { id: 1, type: 'instance', hostname: 'awx', node_type: 'control' },
]}
/>
);
- expect(wrapper.find('button[disabled]')).toHaveLength(1);
+ expect(screen.getByRole('button', { name: 'Disassociate' })).toBeDisabled();
});
- test('should disable button when selected items contain instances thaat are hybrid and are inside a protected instances', () => {
- const wrapper = mountWithContexts(
+
+ test('should disable button for a hybrid instance inside a protected instance group', () => {
+ renderWithContexts(
{}}
- isProectedInstanceGroup
- itemsToDelete={[
- {
- id: 1,
- hostname: 'awx',
- node_type: 'control',
- },
+ isProtectedInstanceGroup
+ itemsToDisassociate={[
+ { id: 1, type: 'instance', hostname: 'awx', node_type: 'hybrid' },
]}
/>
);
- expect(wrapper.find('button[disabled]')).toHaveLength(1);
+ expect(screen.getByRole('button', { name: 'Disassociate' })).toBeDisabled();
});
});
});
diff --git a/awx/ui/src/components/ErrorDetail/ErrorDetail.test.js b/awx/ui/src/components/ErrorDetail/ErrorDetail.test.js
index 1816f4a2d..8bebf868b 100644
--- a/awx/ui/src/components/ErrorDetail/ErrorDetail.test.js
+++ b/awx/ui/src/components/ErrorDetail/ErrorDetail.test.js
@@ -1,46 +1,57 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen, waitFor } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import ErrorDetail from './ErrorDetail';
+// Build an Error that carries a `response`, like the api errors ErrorDetail
+// renders. Object.prototype.hasOwnProperty(error, 'response') drives the
+// network-error vs stack-trace branch, so the property must live on the
+// instance.
+function makeNetworkError(response) {
+ const error = new Error('request failed');
+ error.response = response;
+ return error;
+}
+
describe('ErrorDetail', () => {
- test('renders the expected content', () => {
- const wrapper = mountWithContexts(
+ test('renders the expandable Details toggle collapsed by default', () => {
+ renderWithContexts(
);
- expect(wrapper).toHaveLength(1);
+ const toggle = screen.getByRole('button', { name: /Details/ });
+ // PF ExpandableSection keeps its content mounted but hidden when collapsed,
+ // so assert the collapsed state via aria-expanded rather than absence.
+ expect(toggle).toHaveAttribute('aria-expanded', 'false');
});
- test('testing errors', () => {
- const wrapper = mountWithContexts(
+
+ test('expands to show the network error message on toggle', async () => {
+ const { user } = renderWithContexts(
);
- act(() => wrapper.find('ExpandableSection').prop('onToggle')());
- wrapper.update();
+
+ await user.click(screen.getByRole('button', { name: /Details/ }));
+
+ await waitFor(() => {
+ expect(screen.getByText('project error')).toBeInTheDocument();
+ });
+ expect(screen.getByText('inventory error')).toBeInTheDocument();
+ // request line includes method, url and status
+ expect(screen.getByText('400')).toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.js b/awx/ui/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.js
index 84e6471c3..369a7418e 100644
--- a/awx/ui/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.js
+++ b/awx/ui/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.js
@@ -1,5 +1,9 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen, waitFor } from '@testing-library/react';
+import {
+ renderWithContexts,
+ assertDetail,
+} from '../../../testUtils/rtlContexts';
import ExecutionEnvironmentDetail from './ExecutionEnvironmentDetail';
@@ -12,34 +16,36 @@ const mockExecutionEnvironment = {
};
describe(' ', () => {
- test('should display execution environment detail', async () => {
- const wrapper = mountWithContexts(
+ test('should display execution environment detail', () => {
+ renderWithContexts(
);
- const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail');
- expect(executionEnvironment).toHaveLength(1);
- expect(executionEnvironment.find('dt').text()).toEqual(
- 'Execution Environment'
- );
- expect(executionEnvironment.find('dd').text()).toEqual(
- mockExecutionEnvironment.name
+ assertDetail('Execution Environment', mockExecutionEnvironment.name);
+ expect(
+ screen.getByRole('link', { name: mockExecutionEnvironment.name })
+ ).toHaveAttribute(
+ 'href',
+ `/execution_environments/${mockExecutionEnvironment.id}/details`
);
});
test('should display warning deleted execution environment', async () => {
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail');
- expect(executionEnvironment).toHaveLength(1);
- expect(executionEnvironment.find('dt').text()).toEqual(
- 'Execution Environment'
- );
- expect(executionEnvironment.find('dd').text()).toEqual('Missing resource');
- expect(wrapper.find('Tooltip').prop('content')).toEqual(
- `Execution environment is missing or deleted.`
- );
+ assertDetail('Execution Environment', 'Missing resource');
+ // The deleted-EE warning wraps the icon in a PF Tooltip; its content is
+ // only rendered into the DOM on hover (DOM equivalent of the enzyme
+ // `Tooltip.prop('content')` assertion).
+ const term = screen.getByText('Execution Environment');
+ const icon = term.nextElementSibling.querySelector('svg');
+ await user.hover(icon);
+ await waitFor(() => {
+ expect(
+ screen.getByText('Execution environment is missing or deleted.')
+ ).toBeInTheDocument();
+ });
});
});
diff --git a/awx/ui/src/components/ExpandCollapse/ExpandCollapse.test.js b/awx/ui/src/components/ExpandCollapse/ExpandCollapse.test.js
index eb06b49c5..e9ad95efd 100644
--- a/awx/ui/src/components/ExpandCollapse/ExpandCollapse.test.js
+++ b/awx/ui/src/components/ExpandCollapse/ExpandCollapse.test.js
@@ -1,19 +1,42 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import ExpandCollapse from './ExpandCollapse';
describe(' ', () => {
const onCompact = jest.fn();
const onExpand = jest.fn();
- const isCompact = false;
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
test('initially renders without crashing', () => {
- const wrapper = mountWithContexts(
+ renderWithContexts(
);
- expect(wrapper.length).toBe(1);
+ expect(
+ screen.getByRole('button', { name: 'Collapse' })
+ ).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Expand' })).toBeInTheDocument();
+ });
+
+ test('clicking collapse calls onCompact and clicking expand calls onExpand', async () => {
+ const { user } = renderWithContexts(
+
+ );
+ await user.click(screen.getByRole('button', { name: 'Collapse' }));
+ expect(onCompact).toHaveBeenCalledTimes(1);
+
+ await user.click(screen.getByRole('button', { name: 'Expand' }));
+ expect(onExpand).toHaveBeenCalledTimes(1);
});
});
diff --git a/awx/ui/src/components/FieldWithPrompt/FieldWithPrompt.test.js b/awx/ui/src/components/FieldWithPrompt/FieldWithPrompt.test.js
index e3361a957..eb5aa9892 100644
--- a/awx/ui/src/components/FieldWithPrompt/FieldWithPrompt.test.js
+++ b/awx/ui/src/components/FieldWithPrompt/FieldWithPrompt.test.js
@@ -1,13 +1,12 @@
import React from 'react';
+import { screen } from '@testing-library/react';
import { Field, Formik } from 'formik';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import FieldWithPrompt from './FieldWithPrompt';
describe('FieldWithPrompt', () => {
- let wrapper;
-
test('Required asterisk and Popover hidden when not required and tooltip not provided', () => {
- wrapper = mountWithContexts(
+ const { container } = renderWithContexts(
{
)}
);
- expect(wrapper.find('.pf-c-form__label-required')).toHaveLength(0);
- expect(wrapper.find('Popover')).toHaveLength(0);
+
+ // the prompt-on-launch checkbox is always rendered
+ expect(screen.getByLabelText('Prompt on launch')).toBeInTheDocument();
+ // no required asterisk
+ expect(
+ container.querySelector('.pf-c-form__label-required')
+ ).not.toBeInTheDocument();
+ // no tooltip Popover trigger
+ expect(
+ screen.queryByRole('button', { name: 'More information' })
+ ).not.toBeInTheDocument();
});
test('Required asterisk and Popover shown when required and tooltip provided', () => {
- wrapper = mountWithContexts(
+ const { container } = renderWithContexts(
{
)}
);
- expect(wrapper.find('.pf-c-form__label-required')).toHaveLength(1);
+
+ expect(screen.getByLabelText('Prompt on launch')).toBeInTheDocument();
+ // required asterisk present
+ expect(
+ container.querySelector('.pf-c-form__label-required')
+ ).toBeInTheDocument();
+ // the tooltip Popover renders its trigger button
expect(
- wrapper.find('Popover[data-cy="job-template-limit-tooltip"]').length
- ).toBe(1);
+ screen.getByRole('button', { name: 'More information' })
+ ).toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/HostForm/HostForm.test.js b/awx/ui/src/components/HostForm/HostForm.test.js
index b72d3231c..9ccdc6737 100644
--- a/awx/ui/src/components/HostForm/HostForm.test.js
+++ b/awx/ui/src/components/HostForm/HostForm.test.js
@@ -1,7 +1,7 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
-
+import { screen, waitFor } from '@testing-library/react';
+import { InventoriesAPI } from 'api';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import HostForm from './HostForm';
jest.mock('../../api');
@@ -21,19 +21,17 @@ const mockData = {
};
describe(' ', () => {
- let wrapper;
const handleSubmit = jest.fn();
const handleCancel = jest.fn();
- beforeEach(async () => {
- await act(async () => {
- wrapper = mountWithContexts(
-
- );
+ beforeEach(() => {
+ // the host already has a summary_fields.inventory, so the lookup does not
+ // auto-populate; mock defensively so any stray read resolves quietly
+ InventoriesAPI.read.mockResolvedValue({
+ data: { count: 0, results: [] },
+ });
+ InventoriesAPI.readOptions.mockResolvedValue({
+ data: { actions: { GET: {}, POST: {} }, related_search_fields: [] },
});
});
@@ -42,61 +40,87 @@ describe(' ', () => {
});
test('changing inputs should update form values', async () => {
- await act(async () => {
- wrapper.find('input#host-name').simulate('change', {
- target: { value: 'new foo', name: 'name' },
- });
- wrapper.find('input#host-description').simulate('change', {
- target: { value: 'new bar', name: 'description' },
- });
- });
- wrapper.update();
- expect(wrapper.find('input#host-name').prop('value')).toEqual('new foo');
- expect(wrapper.find('input#host-description').prop('value')).toEqual(
- 'new bar'
+ const { user } = renderWithContexts(
+
);
- expect(wrapper.find('InventoryLookup').prop('isDisabled')).toEqual(false);
+
+ // FormField labelIcon breaks getByLabelText, so query inputs by id
+ const nameInput = document.querySelector('input#host-name');
+ const descriptionInput = document.querySelector('input#host-description');
+
+ await user.clear(nameInput);
+ await user.type(nameInput, 'new foo');
+ await user.clear(descriptionInput);
+ await user.type(descriptionInput, 'new bar');
+
+ expect(nameInput).toHaveValue('new foo');
+ expect(descriptionInput).toHaveValue('new bar');
+ // inventory lookup is enabled (not disabled) — its Search button is enabled
+ expect(screen.getByRole('button', { name: 'Search' })).toBeEnabled();
});
test('calls handleSubmit when form submitted', async () => {
+ const { user } = renderWithContexts(
+
+ );
+
expect(handleSubmit).not.toHaveBeenCalled();
- await act(async () => {
- wrapper.find('button[aria-label="Save"]').simulate('click');
- });
- expect(handleSubmit).toHaveBeenCalledTimes(1);
+ await user.click(screen.getByRole('button', { name: 'Save' }));
+ await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
});
- test('calls "handleCancel" when Cancel button is clicked', () => {
+ test('calls "handleCancel" when Cancel button is clicked', async () => {
+ const { user } = renderWithContexts(
+
+ );
+
expect(handleCancel).not.toHaveBeenCalled();
- wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
+ await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(handleCancel).toHaveBeenCalledTimes(1);
});
test('should hide inventory lookup field', async () => {
- await act(async () => {
- wrapper = mountWithContexts(
-
- );
- });
- expect(wrapper.find('InventoryLookupField').length).toBe(0);
+ renderWithContexts(
+
+ );
+
+ await screen.findByRole('button', { name: 'Save' });
+ // with the lookup hidden there is no "Inventory" form group / Search button
+ expect(screen.queryByText('Inventory')).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Search' })
+ ).not.toBeInTheDocument();
});
test('inventory lookup field should be disabled', async () => {
- await act(async () => {
- wrapper = mountWithContexts(
-
- );
- });
- expect(wrapper.find('InventoryLookup').prop('isDisabled')).toEqual(true);
+ renderWithContexts(
+
+ );
+
+ await screen.findByRole('button', { name: 'Save' });
+ // disableInventoryLookup propagates to the lookup's Search button
+ expect(screen.getByRole('button', { name: 'Search' })).toBeDisabled();
});
});
diff --git a/awx/ui/src/components/HostToggle/HostToggle.test.js b/awx/ui/src/components/HostToggle/HostToggle.test.js
index 6c211f7bd..4106d2997 100644
--- a/awx/ui/src/components/HostToggle/HostToggle.test.js
+++ b/awx/ui/src/components/HostToggle/HostToggle.test.js
@@ -1,7 +1,7 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
+import { screen, waitFor } from '@testing-library/react';
import { HostsAPI } from 'api';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import HostToggle from './HostToggle';
jest.mock('../../api');
@@ -25,28 +25,32 @@ const mockHost = {
},
};
+// The PF Switch renders a hidden checkbox input with the aria-label
+const getToggle = () => screen.getByRole('checkbox', { name: 'Toggle host' });
+
describe('', () => {
- test('should should toggle off', async () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should toggle off', async () => {
const onToggle = jest.fn();
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(true);
+ expect(getToggle()).toBeChecked();
- await act(async () => {
- wrapper.find('Switch').invoke('onChange')();
- });
+ await user.click(getToggle());
expect(HostsAPI.update).toHaveBeenCalledWith(1, {
enabled: false,
});
- wrapper.update();
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(false);
+ await waitFor(() => expect(getToggle()).not.toBeChecked());
expect(onToggle).toHaveBeenCalledWith(false);
});
- test('should should toggle on', async () => {
+ test('should toggle on', async () => {
const onToggle = jest.fn();
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
', () => {
onToggle={onToggle}
/>
);
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(false);
+ expect(getToggle()).not.toBeChecked();
- await act(async () => {
- wrapper.find('Switch').invoke('onChange')();
- });
+ await user.click(getToggle());
expect(HostsAPI.update).toHaveBeenCalledWith(1, {
enabled: true,
});
- wrapper.update();
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(true);
+ await waitFor(() => expect(getToggle()).toBeChecked());
expect(onToggle).toHaveBeenCalledWith(true);
});
test('should be enabled', async () => {
- const wrapper = mountWithContexts( );
- expect(wrapper.find('Switch').prop('isDisabled')).toEqual(false);
+ renderWithContexts( );
+ expect(getToggle()).toBeEnabled();
});
test('should be disabled', async () => {
- const wrapper = mountWithContexts(
-
- );
- expect(wrapper.find('Switch').prop('isDisabled')).toEqual(true);
+ renderWithContexts( );
+ expect(getToggle()).toBeDisabled();
});
test('should show error modal', async () => {
HostsAPI.update.mockImplementation(() => {
throw new Error('nope');
});
- const wrapper = mountWithContexts( );
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(true);
+ const { user } = renderWithContexts( );
+ expect(getToggle()).toBeChecked();
- await act(async () => {
- wrapper.find('Switch').invoke('onChange')();
- });
- wrapper.update();
- const modal = wrapper.find('AlertModal');
- expect(modal).toHaveLength(1);
- expect(modal.prop('isOpen')).toEqual(true);
+ await user.click(getToggle());
+ const dialog = await screen.findByRole('dialog');
+ expect(dialog).toBeInTheDocument();
+ expect(screen.getByText('Error!')).toBeInTheDocument();
- act(() => {
- modal.invoke('onClose')();
- });
- wrapper.update();
- expect(wrapper.find('AlertModal')).toHaveLength(0);
+ await user.click(screen.getByRole('button', { name: 'Close' }));
+ await waitFor(() =>
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ );
});
});
diff --git a/awx/ui/src/components/InstanceToggle/InstanceToggle.test.js b/awx/ui/src/components/InstanceToggle/InstanceToggle.test.js
index bb5ad0206..c07a5e011 100644
--- a/awx/ui/src/components/InstanceToggle/InstanceToggle.test.js
+++ b/awx/ui/src/components/InstanceToggle/InstanceToggle.test.js
@@ -1,7 +1,7 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
+import { screen, waitFor } from '@testing-library/react';
import { InstancesAPI } from 'api';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import InstanceToggle from './InstanceToggle';
jest.mock('../../api');
@@ -33,6 +33,9 @@ const mockInstance = {
managed_by_policy: true,
};
+// The PF Switch renders a hidden checkbox input with the aria-label
+const getToggle = () => screen.getByRole('checkbox', { name: 'Toggle instance' });
+
describe('', () => {
const onToggle = jest.fn();
const fetchInstances = jest.fn();
@@ -42,29 +45,26 @@ describe('', () => {
});
test('should show toggle off', async () => {
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(true);
+ expect(getToggle()).toBeChecked();
- await act(async () => {
- wrapper.find('Switch').invoke('onChange')();
- });
+ await user.click(getToggle());
expect(InstancesAPI.update).toHaveBeenCalledWith(1, {
enabled: false,
});
- wrapper.update();
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(false);
+ await waitFor(() => expect(getToggle()).not.toBeChecked());
expect(onToggle).toHaveBeenCalledWith(false);
expect(fetchInstances).toHaveBeenCalledTimes(1);
});
test('should show toggle on', async () => {
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
', () => {
fetchInstances={fetchInstances}
/>
);
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(false);
+ expect(getToggle()).not.toBeChecked();
- await act(async () => {
- wrapper.find('Switch').invoke('onChange')();
- });
+ await user.click(getToggle());
expect(InstancesAPI.update).toHaveBeenCalledWith(1, {
enabled: true,
});
- wrapper.update();
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(true);
+ await waitFor(() => expect(getToggle()).toBeChecked());
expect(onToggle).toHaveBeenCalledWith(true);
expect(fetchInstances).toHaveBeenCalledTimes(1);
});
@@ -92,23 +89,19 @@ describe('', () => {
InstancesAPI.update.mockImplementation(() => {
throw new Error('nope');
});
- const wrapper = mountWithContexts(
-
+ const { user } = renderWithContexts(
+
);
- expect(wrapper.find('Switch').prop('isChecked')).toEqual(true);
+ expect(getToggle()).toBeChecked();
- await act(async () => {
- wrapper.find('Switch').invoke('onChange')();
- });
- wrapper.update();
- const modal = wrapper.find('AlertModal');
- expect(modal).toHaveLength(1);
- expect(modal.prop('isOpen')).toEqual(true);
+ await user.click(getToggle());
+ const dialog = await screen.findByRole('dialog');
+ expect(dialog).toBeInTheDocument();
+ expect(screen.getByText('Error!')).toBeInTheDocument();
- act(() => {
- modal.invoke('onClose')();
- });
- wrapper.update();
- expect(wrapper.find('AlertModal')).toHaveLength(0);
+ await user.click(screen.getByRole('button', { name: 'Close' }));
+ await waitFor(() =>
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ );
});
});
diff --git a/awx/ui/src/components/JobCancelButton/JobCancelButton.test.js b/awx/ui/src/components/JobCancelButton/JobCancelButton.test.js
index 2429da2d0..487544620 100644
--- a/awx/ui/src/components/JobCancelButton/JobCancelButton.test.js
+++ b/awx/ui/src/components/JobCancelButton/JobCancelButton.test.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
+import { screen, waitFor } from '@testing-library/react';
import {
ProjectUpdatesAPI,
AdHocCommandsAPI,
@@ -7,60 +7,66 @@ import {
WorkflowJobsAPI,
JobsAPI,
} from 'api';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import JobCancelButton from './JobCancelButton';
jest.mock('../../api');
describe(' ', () => {
- let wrapper;
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
test('should render properly', () => {
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- expect(wrapper.length).toBe(1);
- expect(wrapper.find('MinusCircleIcon').length).toBe(0);
+ renderWithContexts(
+
+ );
+ // default (non-icon) button renders the "Cancel Job" text, no MinusCircleIcon
+ expect(
+ screen.getByRole('button', { name: 'Title' })
+ ).toBeInTheDocument();
+ expect(screen.getByText('Cancel Job')).toBeInTheDocument();
+ expect(document.querySelector('.pf-c-button svg')).toBeNull();
});
+
test('should render icon button', () => {
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- expect(wrapper.find('MinusCircleIcon').length).toBe(1);
+ renderWithContexts(
+
+ );
+ // the icon variant renders the MinusCircleIcon (an svg) inside the button
+ const button = screen.getByRole('button', { name: 'Title' });
+ expect(button.querySelector('svg')).not.toBeNull();
});
+
test('should call api', async () => {
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- await act(async () => wrapper.find('Button').prop('onClick')(true));
- wrapper.update();
+ const { user } = renderWithContexts(
+
+ );
+ await user.click(screen.getByRole('button', { name: 'Title' }));
- expect(wrapper.find('AlertModal').length).toBe(1);
- await act(() =>
- wrapper.find('Button#cancel-job-confirm-button').prop('onClick')()
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: 'Confirm cancel job' })
+ );
+ await waitFor(() =>
+ expect(ProjectUpdatesAPI.cancel).toHaveBeenCalledWith(1)
);
- expect(ProjectUpdatesAPI.cancel).toHaveBeenCalledWith(1);
});
+
test('should throw error', async () => {
ProjectUpdatesAPI.cancel.mockRejectedValue(
new Error({
@@ -74,107 +80,98 @@ describe(' ', () => {
},
})
);
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- await act(async () => wrapper.find('Button').prop('onClick')(true));
- wrapper.update();
- expect(wrapper.find('AlertModal').length).toBe(1);
- await act(() =>
- wrapper.find('Button#cancel-job-confirm-button').prop('onClick')()
- );
- wrapper.update();
- expect(wrapper.find('ErrorDetail').length).toBe(1);
- expect(wrapper.find('AlertModal[title="Title"]').length).toBe(0);
+ const { user } = renderWithContexts(
+
+ );
+ await user.click(screen.getByRole('button', { name: 'Title' }));
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: 'Confirm cancel job' })
+ );
+
+ // error modal (errorTitle="Error", with ErrorDetail's "Details" expandable)
+ // replaces the confirm modal whose "Confirm cancel job" button is now gone
+ expect(await screen.findByText('Error')).toBeInTheDocument();
+ expect(screen.getByText('Details')).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Confirm cancel job' })
+ ).not.toBeInTheDocument();
});
test('should cancel Ad Hoc Command job', async () => {
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- await act(async () => wrapper.find('Button').prop('onClick')(true));
- wrapper.update();
-
- expect(wrapper.find('AlertModal').length).toBe(1);
- await act(() =>
- wrapper.find('Button#cancel-job-confirm-button').prop('onClick')()
+ const { user } = renderWithContexts(
+
+ );
+ await user.click(screen.getByRole('button', { name: 'Title' }));
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: 'Confirm cancel job' })
+ );
+ await waitFor(() =>
+ expect(AdHocCommandsAPI.cancel).toHaveBeenCalledWith(1)
);
- expect(AdHocCommandsAPI.cancel).toHaveBeenCalledWith(1);
});
test('should cancel system job', async () => {
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- await act(async () => wrapper.find('Button').prop('onClick')(true));
- wrapper.update();
-
- expect(wrapper.find('AlertModal').length).toBe(1);
- await act(() =>
- wrapper.find('Button#cancel-job-confirm-button').prop('onClick')()
+ const { user } = renderWithContexts(
+
+ );
+ await user.click(screen.getByRole('button', { name: 'Title' }));
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: 'Confirm cancel job' })
);
- expect(SystemJobsAPI.cancel).toHaveBeenCalledWith(1);
+ await waitFor(() => expect(SystemJobsAPI.cancel).toHaveBeenCalledWith(1));
});
test('should cancel workflow job', async () => {
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- await act(async () => wrapper.find('Button').prop('onClick')(true));
- wrapper.update();
-
- expect(wrapper.find('AlertModal').length).toBe(1);
- await act(() =>
- wrapper.find('Button#cancel-job-confirm-button').prop('onClick')()
+ const { user } = renderWithContexts(
+
+ );
+ await user.click(screen.getByRole('button', { name: 'Title' }));
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: 'Confirm cancel job' })
+ );
+ await waitFor(() =>
+ expect(WorkflowJobsAPI.cancel).toHaveBeenCalledWith(1)
);
- expect(WorkflowJobsAPI.cancel).toHaveBeenCalledWith(1);
});
- test('should cancel workflow job', async () => {
- act(() => {
- wrapper = mountWithContexts(
-
- );
- });
- await act(async () => wrapper.find('Button').prop('onClick')(true));
- wrapper.update();
- expect(wrapper.find('AlertModal').length).toBe(1);
- await act(() =>
- wrapper.find('Button#cancel-job-confirm-button').prop('onClick')()
+ test('should cancel job with unknown type via JobsAPI', async () => {
+ const { user } = renderWithContexts(
+
+ );
+ await user.click(screen.getByRole('button', { name: 'Title' }));
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ await user.click(
+ screen.getByRole('button', { name: 'Confirm cancel job' })
);
- expect(JobsAPI.cancel).toHaveBeenCalledWith(1);
+ await waitFor(() => expect(JobsAPI.cancel).toHaveBeenCalledWith(1));
});
});
diff --git a/awx/ui/src/components/LabelSelect/LabelSelect.test.js b/awx/ui/src/components/LabelSelect/LabelSelect.test.js
index 395a470cc..96e82e7ff 100644
--- a/awx/ui/src/components/LabelSelect/LabelSelect.test.js
+++ b/awx/ui/src/components/LabelSelect/LabelSelect.test.js
@@ -1,7 +1,7 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
+import { screen, waitFor, within } from '@testing-library/react';
import { LabelsAPI } from 'api';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import LabelSelect from './LabelSelect';
jest.mock('../../api');
@@ -11,38 +11,47 @@ const options = [
{ id: 2, name: 'two' },
];
+// Open the typeahead by clicking its toggle (the Select renders a toggle
+// button labelled "Options menu"), then return the listbox's option nodes.
+async function openAndGetOptions(user) {
+ await user.click(screen.getByRole('button', { name: 'Options menu' }));
+ const listbox = await screen.findByRole('listbox');
+ return within(listbox).getAllByRole('option');
+}
+
describe(' ', () => {
afterEach(() => {
jest.resetAllMocks();
});
test('should fetch labels', async () => {
- LabelsAPI.read.mockReturnValue({
+ LabelsAPI.read.mockResolvedValue({
data: { results: options },
});
- let wrapper;
- await act(async () => {
- wrapper = mountWithContexts(
- {}} onChange={() => {}} />
- );
- });
+ const { user } = renderWithContexts(
+ {}} onChange={() => {}} />
+ );
+ // the toggle is disabled until the labels load
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: 'Options menu' })).toBeEnabled()
+ );
expect(LabelsAPI.read).toHaveBeenCalledTimes(1);
- wrapper.find('SelectToggle').simulate('click');
- const selectOptions = wrapper.find('SelectOption');
+
+ const selectOptions = await openAndGetOptions(user);
expect(selectOptions).toHaveLength(2);
- expect(selectOptions.at(0).prop('value')).toEqual(options[0]);
- expect(selectOptions.at(1).prop('value')).toEqual(options[1]);
+ expect(selectOptions[0]).toHaveTextContent('one');
+ expect(selectOptions[1]).toHaveTextContent('two');
});
test('should fetch two pages labels if present', async () => {
- await LabelsAPI.read.mockResolvedValueOnce({
+ LabelsAPI.read.mockResolvedValueOnce({
data: {
results: options,
next: '/foo?page=2',
},
});
- await LabelsAPI.read.mockResolvedValueOnce({
+ LabelsAPI.read.mockResolvedValueOnce({
data: {
results: [
{ id: 3, name: 'three' },
@@ -50,39 +59,47 @@ describe(' ', () => {
],
},
});
- let wrapper;
- await act(async () => {
- wrapper = mountWithContexts(
- {}} onChange={() => {}} />
- );
- });
- wrapper.update();
+ const { user } = renderWithContexts(
+ {}} onChange={() => {}} />
+ );
- expect(LabelsAPI.read).toHaveBeenCalledTimes(2);
- wrapper.find('SelectToggle').simulate('click');
- const selectOptions = wrapper.find('SelectOption');
+ await waitFor(() => expect(LabelsAPI.read).toHaveBeenCalledTimes(2));
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: 'Options menu' })).toBeEnabled()
+ );
+
+ const selectOptions = await openAndGetOptions(user);
expect(selectOptions).toHaveLength(4);
});
+
test('Generate a label', async () => {
- let wrapper;
const onChange = jest.fn();
- LabelsAPI.read.mockReturnValue({
+ LabelsAPI.read.mockResolvedValue({
data: {
- options,
+ results: options,
},
});
- await act(async () => {
- wrapper = mountWithContexts(
- {}} onChange={onChange} />
- );
- });
- await wrapper.find('Select').invoke('onSelect')({}, 'foo');
+ const { user } = renderWithContexts(
+ {}} onChange={onChange} />
+ );
+
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: 'Options menu' })).toBeEnabled()
+ );
+
+ // typing a non-matching value surfaces an isCreatable "create" option;
+ // selecting it calls onChange with the new {id, name} label
+ const input = screen.getByRole('textbox');
+ await user.type(input, 'foo');
+ const createOption = await screen.findByText(/foo/);
+ await user.click(createOption);
+
expect(onChange).toHaveBeenCalledWith([{ id: 'foo', name: 'foo' }]);
});
+
test('should handle read-only labels', async () => {
- let wrapper;
const onChange = jest.fn();
- LabelsAPI.read.mockReturnValue({
+ LabelsAPI.read.mockResolvedValue({
data: {
results: [
{ id: 1, name: 'read only' },
@@ -90,22 +107,26 @@ describe(' ', () => {
],
},
});
- await act(async () => {
- wrapper = mountWithContexts(
- {}}
- onChange={onChange}
- />
- );
- });
- wrapper.find('SelectToggle').simulate('click');
- const selectOptions = wrapper.find('SelectOption');
+ const { user } = renderWithContexts(
+ {}}
+ onChange={onChange}
+ />
+ );
+
+ await waitFor(() =>
+ expect(screen.getByRole('button', { name: 'Options menu' })).toBeEnabled()
+ );
+
+ const selectOptions = await openAndGetOptions(user);
expect(selectOptions).toHaveLength(2);
- expect(selectOptions.at(0).prop('isDisabled')).toBe(true);
- expect(selectOptions.at(1).prop('isDisabled')).toBe(false);
+ // PF renders a disabled SelectOption with the pf-m-disabled modifier class
+ // (and a disabled checkbox); the enabled option lacks it
+ expect(selectOptions[0]).toHaveClass('pf-m-disabled');
+ expect(selectOptions[1]).not.toHaveClass('pf-m-disabled');
});
});
diff --git a/awx/ui/src/components/ListHeader/ListHeader.test.js b/awx/ui/src/components/ListHeader/ListHeader.test.js
index c4fa88000..76d167a9b 100644
--- a/awx/ui/src/components/ListHeader/ListHeader.test.js
+++ b/awx/ui/src/components/ListHeader/ListHeader.test.js
@@ -1,7 +1,7 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
+import { act } from '@testing-library/react';
import { createMemoryHistory } from 'history';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import ListHeader from './ListHeader';
describe('ListHeader', () => {
@@ -11,48 +11,58 @@ describe('ListHeader', () => {
integerFields: ['id', 'page', 'page_size'],
dateFields: ['modified', 'created'],
};
- const renderToolbarFn = jest.fn();
+
+ // Capture the toolbar props ListHeader passes to its renderToolbar callback
+ // so the handlers (onSort/onSearch/onRemove/clearAllFilters) can be exercised
+ // directly, mirroring the enzyme suite's `toolbar.prop('onX')(...)`.
+ function makeCapturingToolbar() {
+ const captured = {};
+ const renderToolbar = (props) => {
+ Object.assign(captured, props);
+ return
;
+ };
+ return { captured, renderToolbar };
+ }
+
+ const baseProps = {
+ qsConfig,
+ searchColumns: [{ name: 'foo', key: 'foo__icontains', isDefault: true }],
+ sortColumns: [{ name: 'foo', key: 'foo' }],
+ };
test('initially renders without crashing', () => {
const history = createMemoryHistory({
initialEntries: ['/organizations/1/teams'],
});
- const wrapper = mountWithContexts(
+ const { container } = renderWithContexts(
,
{ context: { router: { history } } }
);
- expect(wrapper.length).toBe(1);
+ expect(container).toBeInTheDocument();
});
test('should navigate when DataListToolbar calls onSort prop', async () => {
const history = createMemoryHistory({
initialEntries: ['/organizations/1/teams'],
});
- const wrapper = mountWithContexts(
- ,
+ const { captured, renderToolbar } = makeCapturingToolbar();
+ renderWithContexts(
+ ,
{ context: { router: { history } } }
);
- const toolbar = wrapper.find('DataListToolbar');
- toolbar.prop('onSort')('foo', 'descending');
+ act(() => {
+ captured.onSort('foo', 'descending');
+ });
expect(history.location.search).toEqual('?item.order_by=-foo');
- toolbar.prop('onSort')('foo', 'ascending');
- // since order_by = name is the default, that should be strip out of the search
+ act(() => {
+ captured.onSort('foo', 'ascending');
+ });
+ // since order_by = foo is the default, that should be stripped out of the search
expect(history.location.search).toEqual('');
});
@@ -61,22 +71,15 @@ describe('ListHeader', () => {
const history = createMemoryHistory({
initialEntries: [`/organizations/1/teams${query}`],
});
- const wrapper = mountWithContexts(
- ,
+ const { captured, renderToolbar } = makeCapturingToolbar();
+ renderWithContexts(
+ ,
{ context: { router: { history } } }
);
expect(history.location.search).toEqual(query);
- const toolbar = wrapper.find('DataListToolbar');
act(() => {
- toolbar.prop('clearAllFilters')();
+ captured.clearAllFilters();
});
expect(history.location.search).toEqual('?item.page_size=5');
});
@@ -86,21 +89,16 @@ describe('ListHeader', () => {
const history = createMemoryHistory({
initialEntries: [`/organizations/1/teams${query}`],
});
- const wrapper = mountWithContexts(
- ,
+ const { captured, renderToolbar } = makeCapturingToolbar();
+ renderWithContexts(
+ ,
{ context: { router: { history } } }
);
expect(history.location.search).toEqual(query);
- const toolbar = wrapper.find('DataListToolbar');
- toolbar.prop('onSearch')('name__icontains', 'foo');
+ act(() => {
+ captured.onSearch('name__icontains', 'foo');
+ });
expect(history.location.search).toEqual(
'?item.name__icontains=foo&item.page_size=10'
);
@@ -111,21 +109,16 @@ describe('ListHeader', () => {
const history = createMemoryHistory({
initialEntries: [`/organizations/1/teams${query}`],
});
- const wrapper = mountWithContexts(
- ,
+ const { captured, renderToolbar } = makeCapturingToolbar();
+ renderWithContexts(
+ ,
{ context: { router: { history } } }
);
expect(history.location.search).toEqual(query);
- const toolbar = wrapper.find('DataListToolbar');
- toolbar.prop('onRemove')('name__icontains', 'foo');
+ act(() => {
+ captured.onRemove('name__icontains', 'foo');
+ });
expect(history.location.search).toEqual('?item.page_size=10');
});
});
diff --git a/awx/ui/src/components/MultiButtonToggle/MultiButtonToggle.test.js b/awx/ui/src/components/MultiButtonToggle/MultiButtonToggle.test.js
index c37e43b72..3c3b29fff 100644
--- a/awx/ui/src/components/MultiButtonToggle/MultiButtonToggle.test.js
+++ b/awx/ui/src/components/MultiButtonToggle/MultiButtonToggle.test.js
@@ -1,35 +1,41 @@
import React from 'react';
-import { shallow } from 'enzyme';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import MultiButtonToggle from './MultiButtonToggle';
describe(' ', () => {
- let wrapper;
const onChange = jest.fn();
- beforeAll(() => {
- wrapper = shallow(
+ const renderToggle = (value = 'yaml') =>
+ render(
);
+
+ afterEach(() => {
+ jest.clearAllMocks();
});
it('should render buttons successfully', () => {
- const buttons = wrapper.find('SmallButton');
- expect(buttons.length).toBe(2);
- expect(buttons.at(0).props().variant).toBe('primary');
- expect(buttons.at(1).props().variant).toBe('secondary');
+ renderToggle();
+ const yamlButton = screen.getByRole('button', { name: 'YAML' });
+ const jsonButton = screen.getByRole('button', { name: 'JSON' });
+ // variant="primary" renders the pf-m-primary modifier; "secondary" the pf-m-secondary one
+ expect(yamlButton).toHaveClass('pf-m-primary');
+ expect(jsonButton).toHaveClass('pf-m-secondary');
});
- it('should call onChange function when button clicked', () => {
- const buttons = wrapper.find('SmallButton');
- buttons.at(1).simulate('click');
+ it('should call onChange function when button clicked', async () => {
+ const user = userEvent.setup();
+ renderToggle();
+ await user.click(screen.getByRole('button', { name: 'JSON' }));
expect(onChange).toHaveBeenCalledWith('json');
});
});
diff --git a/awx/ui/src/components/MultiSelect/TagMultiSelect.test.js b/awx/ui/src/components/MultiSelect/TagMultiSelect.test.js
index 11ebbac11..6e1350381 100644
--- a/awx/ui/src/components/MultiSelect/TagMultiSelect.test.js
+++ b/awx/ui/src/components/MultiSelect/TagMultiSelect.test.js
@@ -1,34 +1,48 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen, within } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import TagMultiSelect from './TagMultiSelect';
+function getChipGroup() {
+ return screen.getByRole('group', { name: 'Chip group category' });
+}
+
describe(' ', () => {
- it('should render Select', () => {
- const wrapper = mountWithContexts(
-
- );
- wrapper.find('input').simulate('focus');
- const options = wrapper.find('Chip');
- expect(options).toHaveLength(2);
- expect(options.at(0).text()).toEqual('foo');
- expect(options.at(1).text()).toEqual('bar');
+ it('should render Select with a chip per value', () => {
+ renderWithContexts( );
+ const chips = within(getChipGroup()).getAllByRole('listitem');
+ expect(chips).toHaveLength(2);
+ expect(chips[0]).toHaveTextContent('foo');
+ expect(chips[1]).toHaveTextContent('bar');
});
- it('should not treat empty string as an option', () => {
- const wrapper = mountWithContexts( );
- wrapper.find('SelectToggle').simulate('click');
- expect(wrapper.find('Select').prop('isOpen')).toEqual(true);
- expect(wrapper.find('Chip')).toHaveLength(0);
+ it('should not treat empty string as an option', async () => {
+ const { user } = renderWithContexts(
+
+ );
+ expect(screen.queryByRole('group', { name: 'Chip group category' })).toBeNull();
+
+ await user.click(screen.getByRole('button', { name: 'Options menu' }));
+ expect(
+ screen.getByRole('button', { name: 'Options menu' })
+ ).toHaveAttribute('aria-expanded', 'true');
+ expect(
+ screen.queryByRole('group', { name: 'Chip group category' })
+ ).toBeNull();
});
- it('should trigger onChange', () => {
+ it('should trigger onChange when an existing option is selected', async () => {
const onChange = jest.fn();
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- wrapper.find('input').simulate('focus');
- wrapper.find('Select').invoke('onSelect')(null, 'baz');
+ await user.click(screen.getByRole('button', { name: 'Options menu' }));
+ // selecting an unselected typed option adds it to the value string
+ const input = screen.getByRole('textbox');
+ await user.type(input, 'baz');
+ await user.click(screen.getByRole('option', { name: /Create.*baz/ }));
+
expect(onChange).toHaveBeenCalledWith('foo,bar,baz');
});
});
diff --git a/awx/ui/src/components/OptionsList/OptionsList.test.js b/awx/ui/src/components/OptionsList/OptionsList.test.js
index db8954357..af9472d73 100644
--- a/awx/ui/src/components/OptionsList/OptionsList.test.js
+++ b/awx/ui/src/components/OptionsList/OptionsList.test.js
@@ -1,18 +1,20 @@
import React from 'react';
+import { screen, within } from '@testing-library/react';
import { getQSConfig } from 'util/qs';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import OptionsList from './OptionsList';
const qsConfig = getQSConfig('test', { order_by: 'foo' });
+const options = [
+ { id: 1, name: 'foo', url: '/item/1' },
+ { id: 2, name: 'bar', url: '/item/2' },
+ { id: 3, name: 'baz', url: '/item/3' },
+];
+
describe(' ', () => {
it('should display list of options', () => {
- const options = [
- { id: 1, name: 'foo', url: '/item/1' },
- { id: 2, name: 'bar', url: '/item/2' },
- { id: 3, name: 'baz', url: '/item/3' },
- ];
- const wrapper = mountWithContexts(
+ renderWithContexts(
', () => {
name="Item"
/>
);
- expect(wrapper.find('PaginatedTable').prop('items')).toEqual(options);
- expect(wrapper.find('SelectedList')).toHaveLength(0);
+
+ // one selectable (radio) row rendered per option
+ expect(screen.getAllByRole('radio')).toHaveLength(options.length);
+ options.forEach((opt) => {
+ expect(screen.getByText(opt.name)).toBeInTheDocument();
+ });
+
+ // no selection preview when value is empty
+ expect(
+ screen.queryByRole('group', { name: 'Chip group category' })
+ ).toBeNull();
});
it('should render selected list', () => {
- const options = [
- { id: 1, name: 'foo', url: '/item/1' },
- { id: 2, name: 'bar', url: '/item/2' },
- { id: 3, name: 'baz', url: '/item/3' },
- ];
- const wrapper = mountWithContexts(
+ renderWithContexts(
', () => {
name="Item"
/>
);
- const list = wrapper.find('SelectedList');
- expect(list).toHaveLength(1);
- expect(list.prop('selected')).toEqual([options[1]]);
+
+ // SelectedList renders its label and a chip for each selected item
+ expect(screen.getByText('Selected')).toBeInTheDocument();
+ const chipGroup = screen.getByRole('group', {
+ name: 'Chip group category',
+ });
+ const chips = within(chipGroup).getAllByRole('listitem');
+ expect(chips).toHaveLength(1);
+ expect(chips[0]).toHaveTextContent('bar');
});
});
diff --git a/awx/ui/src/components/Pagination/Pagination.test.js b/awx/ui/src/components/Pagination/Pagination.test.js
index 20e3d3c48..1ef78e779 100644
--- a/awx/ui/src/components/Pagination/Pagination.test.js
+++ b/awx/ui/src/components/Pagination/Pagination.test.js
@@ -1,11 +1,14 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import Pagination from './Pagination';
describe('Pagination', () => {
test('renders the expected content', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
+ renderWithContexts( );
+ expect(
+ screen.getByRole('navigation', { name: 'Pagination' })
+ ).toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.test.js b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.test.js
index 5c0a75c12..e07a7e065 100644
--- a/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.test.js
+++ b/awx/ui/src/components/RelatedTemplateList/RelatedTemplateList.test.js
@@ -1,10 +1,7 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { JobTemplatesAPI, UnifiedJobTemplatesAPI } from 'api';
-import {
- mountWithContexts,
- waitForElement,
-} from '../../../testUtils/enzymeHelpers';
+import { screen, waitFor, within } from '@testing-library/react';
+import { JobTemplatesAPI } from 'api';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import RelatedTemplateList from './RelatedTemplateList';
@@ -48,7 +45,12 @@ const mockTemplates = [
},
];
-describe(' ', () => {
+function rowCheckbox(name) {
+ const row = screen.getByText(name).closest('tr');
+ return within(row).getByRole('checkbox');
+}
+
+describe(' ', () => {
let debug;
beforeEach(() => {
JobTemplatesAPI.read.mockResolvedValue({
@@ -63,7 +65,7 @@ describe(' ', () => {
actions: [],
},
});
- debug = global.console.debug; // eslint-disable-line prefer-destructuring
+ debug = global.console.debug;
global.console.debug = () => {};
});
@@ -73,204 +75,110 @@ describe(' ', () => {
});
test('Templates are retrieved from the api and the components finishes loading', async () => {
- let wrapper;
- await act(async () => {
- wrapper = mountWithContexts(
-
- );
- });
+ renderWithContexts(
+
+ );
+
+ await screen.findByText('Job Template 1');
+
expect(JobTemplatesAPI.read).toHaveBeenCalledWith({
credentials__id: 1,
order_by: 'name',
page: 1,
page_size: 20,
});
- await act(async () => {
- await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
+ mockTemplates.forEach((tmpl) => {
+ expect(screen.getByText(tmpl.name)).toBeInTheDocument();
});
- expect(wrapper.find('TemplateListItem').length).toEqual(
- mockTemplates.length
- );
});
test('handleSelect is called when a template list item is selected', async () => {
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- await act(async () => {
- await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
- });
- const checkBox = wrapper.find('TemplateListItem').at(1).find('input');
-
- checkBox.simulate('change', {
- target: {
- id: 2,
- name: 'Job Template 2',
- url: '/templates/job_template/2',
- type: 'job_template',
- summary_fields: { user_capabilities: { delete: true } },
- },
- });
+ await screen.findByText('Job Template 2');
- expect(wrapper.find('TemplateListItem').at(1).prop('isSelected')).toBe(
- true
- );
+ const checkbox = rowCheckbox('Job Template 2');
+ expect(checkbox).not.toBeChecked();
+ await user.click(checkbox);
+ expect(rowCheckbox('Job Template 2')).toBeChecked();
});
- test('handleSelectAll is called when a template list item is selected', async () => {
- const wrapper = mountWithContexts(
+ test('handleSelectAll is called when select all is checked', async () => {
+ const { user } = renderWithContexts(
);
- await act(async () => {
- await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
- });
- expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(false);
+ await screen.findByText('Job Template 1');
+
+ const selectAll = screen.getByRole('checkbox', { name: 'Select all' });
+ expect(selectAll).not.toBeChecked();
- const toolBarCheckBox = wrapper.find('Checkbox#select-all');
- act(() => {
- toolBarCheckBox.prop('onChange')(true);
+ await user.click(selectAll);
+ expect(screen.getByRole('checkbox', { name: 'Select all' })).toBeChecked();
+ mockTemplates.forEach((tmpl) => {
+ expect(rowCheckbox(tmpl.name)).toBeChecked();
});
- wrapper.update();
- expect(wrapper.find('Checkbox#select-all').prop('isChecked')).toBe(true);
});
test('delete button is disabled if user does not have delete capabilities on a selected template', async () => {
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- await act(async () => {
- await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
- });
- const deleteAbleItem = wrapper.find('TemplateListItem').at(0).find('input');
- const nonDeleteAbleItem = wrapper
- .find('TemplateListItem')
- .at(2)
- .find('input');
+ await screen.findByText('Job Template 1');
- deleteAbleItem.simulate('change', {
- id: 1,
- name: 'Job Template 1',
- url: '/templates/job_template/1',
- type: 'job_template',
- summary_fields: {
- user_capabilities: {
- delete: true,
- },
- },
- });
+ // with a delete-capable template selected, Delete is enabled
+ await user.click(rowCheckbox('Job Template 1'));
+ expect(screen.getByRole('button', { name: 'Delete' })).toBeEnabled();
- expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
- false
- );
- deleteAbleItem.simulate('change', {
- id: 1,
- name: 'Job Template 1',
- url: '/templates/job_template/1',
- type: 'job_template',
- summary_fields: {
- user_capabilities: {
- delete: true,
- },
- },
- });
- expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
- true
- );
- nonDeleteAbleItem.simulate('change', {
- id: 5,
- name: 'Workflow Job Template 2',
- url: '/templates/workflow_job_template/5',
- type: 'workflow_job_template',
- summary_fields: {
- user_capabilities: {
- delete: false,
- },
- },
- });
- expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe(
- true
- );
+ // adding a template without delete capability disables Delete
+ await user.click(rowCheckbox('Job Template 3'));
+ expect(screen.getByRole('button', { name: 'Delete' })).toBeDisabled();
});
test('api is called to delete templates for each selected template.', async () => {
- const wrapper = mountWithContexts(
+ JobTemplatesAPI.destroy.mockResolvedValue({});
+ const { user } = renderWithContexts(
);
- await act(async () => {
- await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
- });
- const jobTemplate = wrapper.find('TemplateListItem').at(1).find('input');
+ await screen.findByText('Job Template 2');
- jobTemplate.simulate('change', {
- target: {
- id: 2,
- name: 'Job Template 2',
- url: '/templates/job_template/2',
- type: 'job_template',
- summary_fields: { user_capabilities: { delete: true } },
- },
- });
+ await user.click(rowCheckbox('Job Template 2'));
+ await user.click(screen.getByRole('button', { name: 'Delete' }));
+ await user.click(
+ await screen.findByRole('button', { name: 'confirm delete' })
+ );
- await act(async () => {
- wrapper.find('button[aria-label="Delete"]').prop('onClick')();
- });
- wrapper.update();
- await act(async () => {
- await wrapper
- .find('button[aria-label="confirm delete"]')
- .prop('onClick')();
- });
- expect(JobTemplatesAPI.destroy).toHaveBeenCalledWith(2);
+ await waitFor(() =>
+ expect(JobTemplatesAPI.destroy).toHaveBeenCalledWith(2)
+ );
});
test('error is shown when template not successfully deleted from api', async () => {
- JobTemplatesAPI.destroy.mockRejectedValue(
- new Error({
- response: {
- config: {
- method: 'delete',
- url: '/api/v2/job_templates/1',
- },
- data: 'An error occurred',
- },
- })
+ JobTemplatesAPI.destroy.mockRejectedValue(new Error());
+ const { user } = renderWithContexts(
+
);
- let wrapper;
-
- await act(async () => {
- wrapper = mountWithContexts(
-
- );
- });
- wrapper.update();
+ await screen.findByText('Job Template 1');
expect(JobTemplatesAPI.read).toHaveBeenCalledTimes(1);
- await act(async () => {
- wrapper.find('TemplateListItem').at(0).invoke('onSelect')();
- });
- wrapper.update();
-
- await act(async () => {
- wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
- });
- wrapper.update();
+ await user.click(rowCheckbox('Job Template 1'));
+ await user.click(screen.getByRole('button', { name: 'Delete' }));
+ await user.click(
+ await screen.findByRole('button', { name: 'confirm delete' })
+ );
- const modal = wrapper.find('Modal');
- expect(modal).toHaveLength(1);
- expect(modal.prop('title')).toEqual('Error!');
+ expect(await screen.findByText('Error!')).toBeInTheDocument();
});
test('should properly copy template', async () => {
JobTemplatesAPI.copy.mockResolvedValue({});
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- await act(async () => {
- await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
- });
- await act(async () =>
- wrapper.find('Button[aria-label="Copy"]').prop('onClick')()
- );
- expect(JobTemplatesAPI.copy).toHaveBeenCalled();
+ await screen.findByText('Job Template 1');
+
+ await user.click(screen.getByRole('button', { name: 'Copy' }));
+
+ await waitFor(() => expect(JobTemplatesAPI.copy).toHaveBeenCalled());
});
});
diff --git a/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js b/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js
index 50ea827d6..cd28f5b75 100644
--- a/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js
+++ b/awx/ui/src/components/RoutedTabs/RoutedTabs.test.js
@@ -1,12 +1,11 @@
-/* eslint-disable react/jsx-pascal-case */
+
import React from 'react';
+import { screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { Routes, Route } from 'react-router-dom-v5-compat';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import RoutedTabs from './RoutedTabs';
-let wrapper;
-let history;
-
const tabs = [
{ name: 'Details', link: '/organizations/19/details', id: 1 },
{ name: 'Access', link: '/organizations/19/access', id: 2 },
@@ -14,35 +13,54 @@ const tabs = [
{ name: 'Notification', link: '/organizations/19/notification', id: 4 },
];
-describe(' ', () => {
- beforeEach(() => {
- history = createMemoryHistory({
- initialEntries: ['/organizations/19/teams'],
- });
- wrapper = mountWithContexts( , {
- context: { router: { history } },
- });
+function renderTabs(initialEntry) {
+ const history = createMemoryHistory({
+ initialEntries: [initialEntry],
});
+ const utils = renderWithContexts(
+
+ }
+ />
+ ,
+ {
+ context: { router: { history } },
+ }
+ );
+ return { ...utils, history };
+}
+describe(' ', () => {
test('RoutedTabs renders successfully', () => {
- expect(wrapper.find('Tabs li')).toHaveLength(4);
+ renderTabs('/organizations/19/teams');
+ expect(screen.getAllByRole('tab')).toHaveLength(4);
});
- test('Given a URL the correct tab is active', async () => {
+ test('Given a URL the correct tab is active', () => {
+ const { history } = renderTabs('/organizations/19/teams');
expect(history.location.pathname).toEqual('/organizations/19/teams');
- expect(wrapper.find('Tabs').prop('activeKey')).toBe(3);
+ expect(screen.getByRole('tab', { name: 'Teams' })).toHaveAttribute(
+ 'aria-selected',
+ 'true'
+ );
+ expect(screen.getByRole('tab', { name: 'Access' })).toHaveAttribute(
+ 'aria-selected',
+ 'false'
+ );
});
test('should update history when new tab selected', async () => {
- wrapper.find('Tabs').invoke('onSelect')(
- {
- preventDefault: () => {},
- },
- 2
- );
- wrapper.update();
+ const { history, user } = renderTabs('/organizations/19/teams');
- expect(history.location.pathname).toEqual('/organizations/19/access');
- expect(wrapper.find('Tabs').prop('activeKey')).toBe(2);
+ await user.click(screen.getByRole('tab', { name: 'Access' }));
+
+ await waitFor(() =>
+ expect(history.location.pathname).toEqual('/organizations/19/access')
+ );
+ expect(screen.getByRole('tab', { name: 'Access' })).toHaveAttribute(
+ 'aria-selected',
+ 'true'
+ );
});
});
diff --git a/awx/ui/src/components/ScreenHeader/ScreenHeader.test.js b/awx/ui/src/components/ScreenHeader/ScreenHeader.test.js
index a0ba2b35d..83f4349ec 100644
--- a/awx/ui/src/components/ScreenHeader/ScreenHeader.test.js
+++ b/awx/ui/src/components/ScreenHeader/ScreenHeader.test.js
@@ -1,16 +1,11 @@
import React from 'react';
+import { screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
-
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import ScreenHeader from './ScreenHeader';
describe(' ', () => {
- let breadcrumbWrapper;
- let breadcrumb;
- let breadcrumbItem;
- let breadcrumbHeading;
-
const config = {
'/foo': 'Foo',
'/foo/1': 'One',
@@ -18,27 +13,21 @@ describe(' ', () => {
'/foo/1/bar/fiz': 'Fiz',
};
- const findChildren = () => {
- breadcrumb = breadcrumbWrapper.find('ScreenHeader');
- breadcrumbItem = breadcrumbWrapper.find('BreadcrumbItem');
- breadcrumbHeading = breadcrumbWrapper.find('Title');
- };
-
test('initially renders successfully', () => {
- breadcrumbWrapper = mountWithContexts(
+ renderWithContexts(
);
- findChildren();
+ const nav = screen.getByRole('navigation', { name: 'Breadcrumb' });
+ const crumbs = within(nav).getAllByRole('link');
+ expect(crumbs).toHaveLength(2);
+ expect(crumbs[0]).toHaveTextContent('Foo');
+ expect(crumbs[1]).toHaveTextContent('One');
- expect(breadcrumb).toHaveLength(1);
- expect(breadcrumbItem).toHaveLength(2);
- expect(breadcrumbHeading).toHaveLength(1);
- expect(breadcrumbItem.first().text()).toBe('Foo');
- expect(breadcrumbItem.last().text()).toBe('One');
- expect(breadcrumbHeading.text()).toBe('Bar');
+ const heading = screen.getByRole('heading', { level: 2 });
+ expect(heading).toHaveTextContent('Bar');
});
test('renders breadcrumb items defined in breadcrumbConfig', () => {
@@ -52,15 +41,17 @@ describe(' ', () => {
];
routes.forEach(([location, crumbLength]) => {
- breadcrumbWrapper = mountWithContexts(
+ const { unmount } = renderWithContexts(
);
- expect(breadcrumbWrapper.find('BreadcrumbItem')).toHaveLength(
- crumbLength
- );
+ const nav = screen.queryByRole('navigation', { name: 'Breadcrumb' });
+ const crumbs = nav ? within(nav).queryAllByRole('link') : [];
+ expect(crumbs).toHaveLength(crumbLength);
+
+ unmount();
});
});
});
diff --git a/awx/ui/src/components/SelectableCard/SelectableCard.test.js b/awx/ui/src/components/SelectableCard/SelectableCard.test.js
index 75cab3dee..9549f2cd0 100644
--- a/awx/ui/src/components/SelectableCard/SelectableCard.test.js
+++ b/awx/ui/src/components/SelectableCard/SelectableCard.test.js
@@ -1,17 +1,22 @@
import React from 'react';
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import SelectableCard from './SelectableCard';
describe(' ', () => {
- let wrapper;
const onClick = jest.fn();
+
test('initially renders without crashing when not selected', () => {
- wrapper = shallow( );
- expect(wrapper.length).toBe(1);
+ renderWithContexts(
+
+ );
+ expect(screen.getByRole('button', { name: 'card' })).toBeInTheDocument();
});
test('initially renders without crashing when selected', () => {
- wrapper = shallow( );
- expect(wrapper.length).toBe(1);
+ renderWithContexts(
+
+ );
+ expect(screen.getByRole('button', { name: 'card' })).toBeInTheDocument();
});
});
diff --git a/awx/ui/src/components/Sort/Sort.test.js b/awx/ui/src/components/Sort/Sort.test.js
index e89a2a6b4..6c8c19920 100644
--- a/awx/ui/src/components/Sort/Sort.test.js
+++ b/awx/ui/src/components/Sort/Sort.test.js
@@ -1,9 +1,6 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
-import {
- mountWithContexts,
- waitForElement,
-} from '../../../testUtils/enzymeHelpers';
+import { screen, waitFor } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import Sort from './Sort';
@@ -15,15 +12,7 @@ jest.mock('react-router-dom', () => ({
}));
describe(' ', () => {
- let sort;
-
- afterEach(() => {
- if (sort) {
- sort = null;
- }
- });
-
- test('should trigger onSort callback', () => {
+ test('should trigger onSort callback', async () => {
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
@@ -37,21 +26,19 @@ describe(' ', () => {
},
];
- const sortBtn = 'button[aria-label="Sort"]';
-
const onSort = jest.fn();
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
- ).find('Sort');
+ );
- wrapper.find(sortBtn).simulate('click');
+ await user.click(screen.getByRole('button', { name: 'Sort' }));
expect(onSort).toHaveBeenCalledTimes(1);
expect(onSort).toHaveBeenCalledWith('name', 'descending');
});
- test('onSort properly passes back descending when ascending was passed as prop', () => {
+ test('onSort properly passes back descending when ascending was passed as prop', async () => {
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
@@ -59,32 +46,21 @@ describe(' ', () => {
};
const columns = [
- {
- name: 'Foo',
- key: 'foo',
- },
- {
- name: 'Bar',
- key: 'bar',
- },
- {
- name: 'Bakery',
- key: 'bakery',
- },
+ { name: 'Foo', key: 'foo' },
+ { name: 'Bar', key: 'bar' },
+ { name: 'Bakery', key: 'bakery' },
];
const onSort = jest.fn();
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
- ).find('Sort');
- const sortDropdownToggle = wrapper.find('Button');
- expect(sortDropdownToggle.length).toBe(1);
- sortDropdownToggle.simulate('click');
+ );
+ await user.click(screen.getByRole('button', { name: 'Sort' }));
expect(onSort).toHaveBeenCalledWith('foo', 'descending');
});
- test('onSort properly passes back ascending when descending was passed as prop', () => {
+ test('onSort properly passes back ascending when descending was passed as prop', async () => {
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: '-foo' },
@@ -92,28 +68,17 @@ describe(' ', () => {
};
const columns = [
- {
- name: 'Foo',
- key: 'foo',
- },
- {
- name: 'Bar',
- key: 'bar',
- },
- {
- name: 'Bakery',
- key: 'bakery',
- },
+ { name: 'Foo', key: 'foo' },
+ { name: 'Bar', key: 'bar' },
+ { name: 'Bakery', key: 'bakery' },
];
const onSort = jest.fn();
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
- ).find('Sort');
- const sortDropdownToggle = wrapper.find('Button');
- expect(sortDropdownToggle.length).toBe(1);
- sortDropdownToggle.simulate('click');
+ );
+ await user.click(screen.getByRole('button', { name: 'Sort' }));
expect(onSort).toHaveBeenCalledWith('foo', 'ascending');
});
@@ -125,37 +90,30 @@ describe(' ', () => {
};
const columns = [
- {
- name: 'Foo',
- key: 'foo',
- },
- {
- name: 'Bar',
- key: 'bar',
- },
- {
- name: 'Bakery',
- key: 'bakery',
- },
+ { name: 'Foo', key: 'foo' },
+ { name: 'Bar', key: 'bar' },
+ { name: 'Bakery', key: 'bakery' },
];
const onSort = jest.fn();
- const wrapper = mountWithContexts(
+ const { user } = renderWithContexts(
);
- act(() => wrapper.find('Dropdown').invoke('onToggle')(true));
- wrapper.update();
- await waitForElement(
- wrapper,
- 'Dropdown',
- (el) => el.prop('isOpen') === true
- );
- act(() =>
- wrapper.find('li').at(0).prop('onClick')({ target: { innerText: 'Bar' } })
+ // Open the sort dropdown (toggle shows the active column name "Foo").
+ await user.click(screen.getByRole('button', { name: 'Foo' }));
+ const barItem = await screen.findByText('Bar');
+ // Sort's handleDropdownSelect matches the picked column by the clicked
+ // element's `innerText`. jsdom does not implement innerText, so define it
+ // explicitly here to drive the real production handler.
+ Object.defineProperty(barItem, 'innerText', {
+ value: 'Bar',
+ configurable: true,
+ });
+ await user.click(barItem);
+ await waitFor(() =>
+ expect(onSort).toHaveBeenCalledWith('bar', 'ascending')
);
- wrapper.update();
- expect(onSort).toHaveBeenCalledWith('bar', 'ascending');
});
test('should display numeric descending icon', () => {
@@ -166,7 +124,7 @@ describe(' ', () => {
};
const numericColumns = [{ name: 'ID', key: 'id' }];
- const wrapper = mountWithContexts(
+ const { container } = renderWithContexts(
', () => {
/>
);
- expect(wrapper.find('SortNumericDownAltIcon')).toHaveLength(1);
+ // SortNumericDownAltIcon
+ const path = container.querySelector('button[aria-label="Sort"] svg path');
+ expect(path.getAttribute('d')).toContain('zm224 64h-16V304');
});
test('should display numeric ascending icon', () => {
@@ -185,7 +145,7 @@ describe(' ', () => {
};
const numericColumns = [{ name: 'ID', key: 'id' }];
- const wrapper = mountWithContexts(
+ const { container } = renderWithContexts(
', () => {
/>
);
- expect(wrapper.find('SortNumericDownIcon')).toHaveLength(1);
+ // SortNumericDownIcon
+ const path = container.querySelector('button[aria-label="Sort"] svg path');
+ expect(path.getAttribute('d')).toContain('M304 96h16v64h-16');
});
test('should display alphanumeric descending icon', () => {
@@ -204,7 +166,7 @@ describe(' ', () => {
};
const alphaColumns = [{ name: 'Name', key: 'name' }];
- const wrapper = mountWithContexts(
+ const { container } = renderWithContexts(
', () => {
/>
);
- expect(wrapper.find('SortAlphaDownAltIcon')).toHaveLength(1);
+ // SortAlphaDownAltIcon
+ const path = container.querySelector('button[aria-label="Sort"] svg path');
+ expect(path.getAttribute('d')).toContain('352zm112-128h128');
});
test('should display alphanumeric ascending icon', () => {
@@ -223,7 +187,7 @@ describe(' ', () => {
};
const alphaColumns = [{ name: 'Name', key: 'name' }];
- const wrapper = mountWithContexts(
+ const { container } = renderWithContexts(
', () => {
/>
);
- expect(wrapper.find('SortAlphaDownIcon')).toHaveLength(1);
+ // SortAlphaDownIcon
+ const path = container.querySelector('button[aria-label="Sort"] svg path');
+ expect(path.getAttribute('d')).toContain('190.22 352 176 352zm240-64H288');
});
});
diff --git a/awx/ui/src/components/Sparkline/Sparkline.test.js b/awx/ui/src/components/Sparkline/Sparkline.test.js
index a19c31e43..852b1fc45 100644
--- a/awx/ui/src/components/Sparkline/Sparkline.test.js
+++ b/awx/ui/src/components/Sparkline/Sparkline.test.js
@@ -1,17 +1,16 @@
import React from 'react';
-
-import {
- mountWithContexts,
- shallowWithContexts,
-} from '../../../testUtils/enzymeHelpers';
+import { screen } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import Sparkline from './Sparkline';
describe('Sparkline', () => {
test('renders the expected content', () => {
- const wrapper = shallowWithContexts( );
- expect(wrapper).toHaveLength(1);
+ const { container } = renderWithContexts( );
+ // No jobs => no icons/links rendered.
+ expect(container.querySelectorAll('a')).toHaveLength(0);
});
+
test('renders an icon with tooltips and links for each job', () => {
const jobs = [
{
@@ -25,9 +24,15 @@ describe('Sparkline', () => {
finished: '2019-08-09T15:27:57.320120Z',
},
];
- const wrapper = mountWithContexts( );
- expect(wrapper.find('StatusIcon')).toHaveLength(2);
- expect(wrapper.find('Tooltip')).toHaveLength(2);
- expect(wrapper.find('Link')).toHaveLength(2);
+ const { container } = renderWithContexts( );
+ // One Link per job, addressed by its accessible name. Each Link wraps a
+ // Tooltip + StatusIcon (the StatusIcon div carries aria-label={status}).
+ const link1 = screen.getByRole('link', { name: 'View job 1' });
+ const link2 = screen.getByRole('link', { name: 'View job 2' });
+ expect(link1).toHaveAttribute('href', '/jobs/undefined/1');
+ expect(link2).toHaveAttribute('href', '/jobs/undefined/2');
+ expect(link1.querySelector('[data-job-status="successful"]')).toBeInTheDocument();
+ expect(link2.querySelector('[data-job-status="failed"]')).toBeInTheDocument();
+ expect(container.querySelectorAll('a')).toHaveLength(2);
});
});
diff --git a/awx/ui/src/components/StatusIcon/StatusIcon.test.js b/awx/ui/src/components/StatusIcon/StatusIcon.test.js
index 783bc8745..af305bcf7 100644
--- a/awx/ui/src/components/StatusIcon/StatusIcon.test.js
+++ b/awx/ui/src/components/StatusIcon/StatusIcon.test.js
@@ -1,59 +1,68 @@
import React from 'react';
-import { mount } from 'enzyme';
+import { render } from '@testing-library/react';
import StatusIcon from './StatusIcon';
+// The PF icon SVGs have role="img" but no distinguishing accessible name,
+// so to preserve the original per-icon assertions we match on the SVG path
+// data, which is unique per icon component. Each prefix below identifies the
+// icon the original enzyme test looked up by component name.
+const ICON_PATH_PREFIX = {
+ CheckCircleIcon: 'M504 256c0 136.967-111.033 248',
+ RunningIcon: 'M370.72 133.28C339.458 104.008',
+ ClockIcon: 'M256,8C119,8,8,119,8,256S119,5',
+ ExclamationCircleIcon: 'M504 256c0 136.997-111.043 248',
+ ExclamationTriangleIcon: 'M569.517 440.013C587.975 472.0',
+ MinusCircleIcon: 'M256 8C119 8 8 119 8 256s111 2',
+};
+
+function expectIcon(container, iconName) {
+ const path = container.querySelector('svg path').getAttribute('d');
+ expect(path).toContain(ICON_PATH_PREFIX[iconName]);
+}
+
describe('StatusIcon', () => {
test('renders the successful status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('CheckCircleIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'CheckCircleIcon');
});
test('renders running status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('RunningIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'RunningIcon');
});
test('renders waiting status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ClockIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'ClockIcon');
});
test('renders failed status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'ExclamationCircleIcon');
});
test('renders a successful status when host status is "ok"', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('CheckCircleIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'CheckCircleIcon');
});
test('renders "failed" host status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'ExclamationCircleIcon');
});
test('renders "changed" host status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'ExclamationTriangleIcon');
});
test('renders "skipped" host status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('MinusCircleIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'MinusCircleIcon');
});
test('renders "unreachable" host status', () => {
- const wrapper = mount( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
+ const { container } = render( );
+ expectIcon(container, 'ExclamationCircleIcon');
});
});
diff --git a/awx/ui/src/components/StatusLabel/StatusLabel.test.js b/awx/ui/src/components/StatusLabel/StatusLabel.test.js
index 3e67af098..7760e65b2 100644
--- a/awx/ui/src/components/StatusLabel/StatusLabel.test.js
+++ b/awx/ui/src/components/StatusLabel/StatusLabel.test.js
@@ -1,89 +1,142 @@
import React from 'react';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import StatusLabel from './StatusLabel';
+// PF Label color is rendered as a `pf-m-` class on the label span, and
+// the status icon (no distinguishing accessible name) is identified by its
+// unique SVG path data — both preserve the original enzyme prop assertions.
+const ICON_PATH_PREFIX = {
+ CheckCircleIcon: 'M504 256c0 136.967-111.033 248',
+ ExclamationCircleIcon: 'M504 256c0 136.997-111.043 248',
+ SyncAltIcon: 'M370.72 133.28C339.458 104.008',
+ ClockIcon: 'M256,8C119,8,8,119,8,256S119,5',
+ MinusCircleIcon: 'M256 8C119 8 8 119 8 256s111 2',
+ ExclamationTriangleIcon: 'M569.517 440.013C587.975 472.0',
+};
+
+function getLabel(container) {
+ return container.querySelector('.pf-c-label');
+}
+
+function expectLabel(container, { icon, color, text }) {
+ const label = getLabel(container);
+ if (color === 'grey') {
+ // PF Label renders no color modifier class for the default grey color, so
+ // assert no other color class leaked in (preserving the original
+ // color="grey" prop assertion).
+ ['green', 'red', 'blue', 'orange'].forEach((c) =>
+ expect(label).not.toHaveClass(`pf-m-${c}`)
+ );
+ } else {
+ expect(label).toHaveClass(`pf-m-${color}`);
+ }
+ expect(label).toHaveTextContent(text);
+ const path = container.querySelector('svg path').getAttribute('d');
+ expect(path).toContain(ICON_PATH_PREFIX[icon]);
+}
+
describe('StatusLabel', () => {
test('should render success', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('CheckCircleIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('green');
- expect(wrapper.text()).toEqual('Success');
- expect(wrapper.find('Tooltip')).toHaveLength(0);
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'CheckCircleIcon',
+ color: 'green',
+ text: 'Success',
+ });
+ // No tooltip wrapper when tooltipContent is absent.
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
test('should render failed', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('red');
- expect(wrapper.text()).toEqual('Failed');
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'ExclamationCircleIcon',
+ color: 'red',
+ text: 'Failed',
+ });
});
test('should render error', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('red');
- expect(wrapper.text()).toEqual('Error');
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'ExclamationCircleIcon',
+ color: 'red',
+ text: 'Error',
+ });
});
test('should render running', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('SyncAltIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('blue');
- expect(wrapper.text()).toEqual('Running');
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'SyncAltIcon',
+ color: 'blue',
+ text: 'Running',
+ });
});
test('should render pending', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ClockIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('blue');
- expect(wrapper.text()).toEqual('Pending');
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'ClockIcon',
+ color: 'blue',
+ text: 'Pending',
+ });
});
test('should render waiting', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ClockIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('grey');
- expect(wrapper.text()).toEqual('Waiting');
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'ClockIcon',
+ color: 'grey',
+ text: 'Waiting',
+ });
});
test('should render disabled', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('MinusCircleIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('grey');
- expect(wrapper.text()).toEqual('Disabled');
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'MinusCircleIcon',
+ color: 'grey',
+ text: 'Disabled',
+ });
});
test('should render canceled', () => {
- const wrapper = mountWithContexts( );
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('orange');
- expect(wrapper.text()).toEqual('Canceled');
+ const { container } = renderWithContexts( );
+ expectLabel(container, {
+ icon: 'ExclamationTriangleIcon',
+ color: 'orange',
+ text: 'Canceled',
+ });
});
- test('should render tooltip', () => {
- const wrapper = mountWithContexts(
+ test('should render tooltip', async () => {
+ const { container } = renderWithContexts(
);
- expect(wrapper).toHaveLength(1);
- expect(wrapper.find('CheckCircleIcon')).toHaveLength(1);
- expect(wrapper.find('Label').prop('color')).toEqual('green');
- expect(wrapper.text()).toEqual('Success');
- expect(wrapper.find('Tooltip')).toHaveLength(1);
- expect(wrapper.find('Tooltip').prop('content')).toEqual('Foo');
+ expectLabel(container, {
+ icon: 'CheckCircleIcon',
+ color: 'green',
+ text: 'Success',
+ });
+ // The Tooltip wrapper renders its content into a portal on hover.
+ fireEvent.mouseEnter(getLabel(container));
+ await waitFor(() =>
+ expect(screen.getByRole('tooltip')).toHaveTextContent('Foo')
+ );
+ fireEvent.mouseLeave(getLabel(container));
+ await waitFor(() =>
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
+ );
});
test('should render children', () => {
- const wrapper = mountWithContexts(
-
+ const { container } = renderWithContexts(
+
+ children
+
);
- expect(wrapper.text()).toEqual('children');
+ expect(getLabel(container)).toHaveTextContent('children');
});
});
diff --git a/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js b/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js
index b82e02b33..95bf0a24b 100644
--- a/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js
+++ b/awx/ui/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.js
@@ -1,10 +1,8 @@
import React from 'react';
-import { act } from 'react-dom/test-utils';
+import { screen, waitFor, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { UsersAPI, JobTemplatesAPI } from 'api';
-import {
- mountWithContexts,
- waitForElement,
-} from '../../../testUtils/enzymeHelpers';
+import { renderWithContexts } from '../../../testUtils/rtlContexts';
import UserAndTeamAccessAdd from './UserAndTeamAccessAdd';
jest.mock('../../api');
@@ -12,179 +10,196 @@ jest.mock('../../api');
const onError = jest.fn();
const onClose = jest.fn();
-describe(' ', () => {
- const resources = {
- data: {
- results: [
- {
- id: 1,
- name: 'Job Template Foo Bar',
- url: '/api/v2/job_template/1/',
- summary_fields: {
- object_roles: {
- admin_role: {
- description: 'Can manage all aspects of the job template',
- name: 'Admin',
- id: 164,
- },
- execute_role: {
- description: 'May run the job template',
- name: 'Execute',
- id: 165,
- },
- read_role: {
- description: 'May view settings for the job template',
- name: 'Read',
- id: 166,
- },
+const resources = {
+ data: {
+ results: [
+ {
+ id: 1,
+ name: 'Job Template Foo Bar',
+ url: '/api/v2/job_template/1/',
+ summary_fields: {
+ object_roles: {
+ admin_role: {
+ description: 'Can manage all aspects of the job template',
+ name: 'Admin',
+ id: 164,
+ },
+ execute_role: {
+ description: 'May run the job template',
+ name: 'Execute',
+ id: 165,
+ },
+ read_role: {
+ description: 'May view settings for the job template',
+ name: 'Read',
+ id: 166,
},
},
},
- ],
- count: 1,
- },
- };
- const options = {
- data: {
- actions: {
- GET: {},
- POST: {},
},
- related_search_fields: [],
+ ],
+ count: 1,
+ },
+};
+const options = {
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
},
- };
- let wrapper;
- beforeEach(async () => {
- await act(async () => {
- wrapper = mountWithContexts(
- {}}
- onClose={onClose}
- title="Add user permissions"
- onError={onError}
- />
- );
- });
- wrapper.update();
+ related_search_fields: [],
+ },
+};
+
+// Returns the PF Wizard footer navigation button by its visible label.
+function footerButton(label) {
+ return screen
+ .getAllByRole('button')
+ .find((b) => b.textContent.trim() === label);
+}
+
+// Returns the wizard nav for a given step name.
+function navItem(name) {
+ return screen
+ .getAllByRole('button')
+ .find(
+ (b) =>
+ b.classList.contains('pf-c-wizard__nav-link') &&
+ b.textContent.trim() === name
+ );
+}
+
+// SelectResourceStep issues a debounced (1000ms) API read on mount; advance
+// timers + flush microtasks so the list renders, without clicking in a
+// retry loop.
+async function settleList() {
+ // advance past the 1000ms debounce with fake timers (instead of sleeping a
+ // real 1.2s) and flush the resulting microtasks so the list renders
+ await act(async () => {
+ await Promise.resolve();
+ });
+ await act(async () => {
+ jest.advanceTimersByTime(1200);
+ });
+ await act(async () => {
+ await Promise.resolve();
});
+}
+
+describe(' ', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
afterEach(() => {
+ jest.useRealTimers();
jest.resetAllMocks();
});
- test('should mount properly', async () => {
- expect(wrapper.find('PFWizard').length).toBe(1);
+
+ function setup() {
+ const utils = renderWithContexts(
+ {}}
+ onClose={onClose}
+ title="Add user permissions"
+ onError={onError}
+ />
+ );
+ // a userEvent bound to the fake timers so its internal delays advance
+ return {
+ ...utils,
+ user: userEvent.setup({ advanceTimers: jest.advanceTimersByTime }),
+ };
+ }
+
+ test('should mount properly', () => {
+ setup();
+ // The PF Wizard renders its first-step nav item and resource cards.
+ expect(navItem('Add resource type')).toBeInTheDocument();
+ expect(
+ screen.getByText('Job templates', { selector: 'b' })
+ ).toBeInTheDocument();
});
+
test('should disable steps', async () => {
- expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
- expect(
- wrapper
- .find('WizardNavItem[content="Select items from list"]')
- .prop('isDisabled')
- ).toBe(true);
- expect(
- wrapper
- .find('WizardNavItem[content="Select roles to apply"]')
- .prop('isDisabled')
- ).toBe(true);
- await act(async () =>
- wrapper.find('SelectableCard[dataCy="add-role-jobTemplate"]').prop('onClick')({
- fetchItems: JobTemplatesAPI.read,
- label: 'Job template',
- selectedResource: 'jobTemplate',
- searchColumns: [
- { name: 'Name', key: 'name__icontains', isDefault: true },
- ],
- sortColumns: [{ name: 'Name', key: 'name' }],
- })
+ const { user } = setup();
+ // Next is disabled and later steps are not jumpable until a resource type
+ // is chosen.
+ expect(footerButton('Next')).toBeDisabled();
+ expect(navItem('Select items from list')).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
);
- await act(async () =>
- wrapper.find('Button[type="submit"]').prop('onClick')()
+ expect(navItem('Select roles to apply')).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
+
+ await user.click(
+ document.querySelector('[data-cy="add-role-jobTemplate"]')
+ );
+ await user.click(footerButton('Next'));
+ await settleList();
+
+ expect(navItem('Add resource type')).not.toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
+ expect(navItem('Select items from list')).not.toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
+ // Step 3 stays disabled until a resource row is selected.
+ expect(navItem('Select roles to apply')).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
);
- wrapper.update();
- expect(
- wrapper
- .find('WizardNavItem[content="Add resource type"]')
- .prop('isDisabled')
- ).toBe(false);
- expect(
- wrapper
- .find('WizardNavItem[content="Select items from list"]')
- .prop('isDisabled')
- ).toBe(false);
- expect(
- wrapper
- .find('WizardNavItem[content="Select roles to apply"]')
- .prop('isDisabled')
- ).toBe(true);
});
test('should call api to associate role', async () => {
JobTemplatesAPI.read.mockResolvedValue(resources);
JobTemplatesAPI.readOptions.mockResolvedValue(options);
UsersAPI.associateRole.mockResolvedValue({});
- await act(async () =>
- wrapper.find('SelectableCard[dataCy="add-role-jobTemplate"]').prop('onClick')({
- fetchItems: JobTemplatesAPI.read,
- fetchOptions: JobTemplatesAPI.readOptions,
- label: 'Job template',
- selectedResource: 'jobTemplate',
- searchColumns: [
- { name: 'Name', key: 'name__icontains', isDefault: true },
- ],
- sortColumns: [{ name: 'Name', key: 'name' }],
- })
- );
- await act(async () =>
- wrapper.find('Button[type="submit"]').prop('onClick')()
+
+ const { user } = setup();
+
+ // Step 1: pick a resource type, advance.
+ await user.click(
+ document.querySelector('[data-cy="add-role-jobTemplate"]')
);
+ await user.click(footerButton('Next'));
+ await settleList();
+
expect(JobTemplatesAPI.read).toHaveBeenCalledWith({
order_by: 'name',
page: 1,
page_size: 5,
});
+ expect(
+ await screen.findByText('Job Template Foo Bar')
+ ).toBeInTheDocument();
- await waitForElement(wrapper, 'SelectResourceStep', (el) => el.length > 0);
- expect(JobTemplatesAPI.read).toHaveBeenCalled();
- await act(async () =>
- wrapper
- .find('CheckboxListItem')
- .first()
- .find('input[type="checkbox"]')
- .simulate('click')
- );
-
- wrapper.update();
-
- await act(async () =>
- wrapper.find('Button[type="submit"]').prop('onClick')()
- );
-
- wrapper.update();
-
- expect(wrapper.find('RolesStep').length).toBe(1);
+ // Step 2: select the fetched resource row, advance.
+ await user.click(document.querySelector('input[name="checkrow0"]'));
+ await user.click(footerButton('Next'));
- await act(async () =>
- wrapper.find('CheckboxCard').first().prop('onSelect')()
- );
+ // Step 3: the roles step renders a checkbox card per object role.
+ const adminCard = await screen.findByRole('checkbox', { name: 'Admin' });
+ await user.click(adminCard);
+ await user.click(footerButton('Save'));
- await act(async () =>
- wrapper.find('Button[type="submit"]').prop('onClick')()
- );
-
- // 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)
+ // associate must use the resourceId passed by the parent screen (99), not a
+ // route param (empty under react-router v6).
+ await waitFor(() =>
+ expect(UsersAPI.associateRole).toHaveBeenCalledWith(99, expect.any(Number))
);
});
test('should close wizard on cancel', async () => {
- await act(async () =>
- wrapper.find('Button[children="Cancel"]').prop('onClick')()
- );
- wrapper.update();
+ const { user } = setup();
+ await user.click(footerButton('Cancel'));
expect(onClose).toHaveBeenCalledTimes(1);
});
@@ -205,51 +220,27 @@ describe(' ', () => {
})
);
- await act(async () =>
- wrapper.find('SelectableCard[dataCy="add-role-jobTemplate"]').prop('onClick')({
- fetchItems: JobTemplatesAPI.read,
- fetchOptions: JobTemplatesAPI.readOptions,
- label: 'Job template',
- selectedResource: 'jobTemplate',
- searchColumns: [
- { name: 'Name', key: 'name__icontains', isDefault: true },
- ],
- sortColumns: [{ name: 'Name', key: 'name' }],
- })
- );
- await act(async () =>
- wrapper.find('Button[type="submit"]').prop('onClick')()
- );
- await waitForElement(wrapper, 'SelectResourceStep', (el) => el.length > 0);
- expect(JobTemplatesAPI.read).toHaveBeenCalled();
- await act(async () =>
- wrapper
- .find('CheckboxListItem')
- .first()
- .find('input[type="checkbox"]')
- .simulate('click')
- );
+ const { user } = setup();
- wrapper.update();
-
- await act(async () =>
- wrapper.find('Button[type="submit"]').prop('onClick')()
+ await user.click(
+ document.querySelector('[data-cy="add-role-jobTemplate"]')
);
+ await user.click(footerButton('Next'));
+ await settleList();
- wrapper.update();
-
- expect(wrapper.find('RolesStep').length).toBe(1);
+ expect(JobTemplatesAPI.read).toHaveBeenCalled();
+ expect(
+ await screen.findByText('Job Template Foo Bar')
+ ).toBeInTheDocument();
- await act(async () =>
- wrapper.find('CheckboxCard').first().prop('onSelect')()
- );
+ await user.click(document.querySelector('input[name="checkrow0"]'));
+ await user.click(footerButton('Next'));
- await act(async () =>
- wrapper.find('Button[type="submit"]').prop('onClick')()
- );
+ const adminCard = await screen.findByRole('checkbox', { name: 'Admin' });
+ await user.click(adminCard);
+ await user.click(footerButton('Save'));
- await expect(UsersAPI.associateRole).toHaveBeenCalled();
- wrapper.update();
- expect(onError).toHaveBeenCalled();
+ await waitFor(() => expect(UsersAPI.associateRole).toHaveBeenCalled());
+ await waitFor(() => expect(onError).toHaveBeenCalled());
});
});