Merge pull request #4992 from keithjgrant/4817-react-router-upgrade

4817 react router upgrade

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2019-10-15 20:29:16 +00:00
committed by GitHub
26 changed files with 425 additions and 8131 deletions

View File

@@ -11596,6 +11596,16 @@
"dom-walk": "^0.1.0" "dom-walk": "^0.1.0"
} }
}, },
"mini-create-react-context": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
"integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==",
"requires": {
"@babel/runtime": "^7.4.0",
"gud": "^1.0.0",
"tiny-warning": "^1.0.2"
}
},
"minimalistic-assert": { "minimalistic-assert": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -13346,25 +13356,13 @@
} }
}, },
"react": { "react": {
"version": "16.8.6", "version": "16.10.2",
"resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz",
"integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==",
"requires": { "requires": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2"
"scheduler": "^0.13.6"
},
"dependencies": {
"scheduler": {
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
}
} }
}, },
"react-codemirror2": { "react-codemirror2": {
@@ -13373,20 +13371,20 @@
"integrity": "sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag==" "integrity": "sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag=="
}, },
"react-dom": { "react-dom": {
"version": "16.8.6", "version": "16.10.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.2.tgz",
"integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", "integrity": "sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw==",
"requires": { "requires": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"scheduler": "^0.13.6" "scheduler": "^0.16.2"
}, },
"dependencies": { "dependencies": {
"scheduler": { "scheduler": {
"version": "0.13.6", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==",
"requires": { "requires": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
@@ -13424,30 +13422,51 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"react-router": { "react-router": {
"version": "4.3.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
"integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==",
"requires": { "requires": {
"history": "^4.7.2", "@babel/runtime": "^7.1.2",
"hoist-non-react-statics": "^2.5.0", "history": "^4.9.0",
"invariant": "^2.2.4", "hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
"mini-create-react-context": "^0.3.0",
"path-to-regexp": "^1.7.0", "path-to-regexp": "^1.7.0",
"prop-types": "^15.6.1", "prop-types": "^15.6.2",
"warning": "^4.0.1" "react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
"requires": {
"react-is": "^16.7.0"
},
"dependencies": {
"react-is": {
"version": "16.10.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz",
"integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA=="
}
}
}
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "4.3.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz",
"integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", "integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==",
"requires": { "requires": {
"history": "^4.7.2", "@babel/runtime": "^7.1.2",
"invariant": "^2.2.4", "history": "^4.9.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
"prop-types": "^15.6.1", "prop-types": "^15.6.2",
"react-router": "^4.3.1", "react-router": "5.1.2",
"warning": "^4.0.1" "tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
} }
}, },
"react-test-renderer": { "react-test-renderer": {
@@ -16299,14 +16318,6 @@
"makeerror": "1.0.x" "makeerror": "1.0.x"
} }
}, },
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": { "watchpack": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",

View File

@@ -69,10 +69,10 @@
"html-entities": "^1.2.1", "html-entities": "^1.2.1",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.10.2",
"react-codemirror2": "^6.0.0", "react-codemirror2": "^6.0.0",
"react-dom": "^16.8.6", "react-dom": "^16.10.2",
"react-router-dom": "^4.3.1", "react-router-dom": "^5.1.2",
"react-virtualized": "^9.21.1", "react-virtualized": "^9.21.1",
"styled-components": "^4.2.0" "styled-components": "^4.2.0"
} }

View File

@@ -1,4 +1,5 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import { global_breakpoint_md } from '@patternfly/react-tokens'; import { global_breakpoint_md } from '@patternfly/react-tokens';
import { import {
Nav, Nav,
@@ -66,8 +67,9 @@ class App extends Component {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
async handleLogout() { async handleLogout() {
const { history } = this.props;
await RootAPI.logout(); await RootAPI.logout();
window.location.replace('/#/login'); history.replace('/login');
} }
handleAboutOpen() { handleAboutOpen() {
@@ -193,4 +195,4 @@ class App extends Component {
} }
export { App as _App }; export { App as _App };
export default withI18n()(App); export default withI18n()(withRouter(App));

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils'; import { sleep } from '@testUtils/testUtils';
@@ -29,10 +30,11 @@ describe('LaunchButton', () => {
); );
expect(wrapper).toHaveLength(1); expect(wrapper).toHaveLength(1);
}); });
test('redirects to job after successful launch', async done => {
const history = { test('should redirect to job after successful launch', async () => {
push: jest.fn(), const history = createMemoryHistory({
}; initialEntries: ['/jobs/9000'],
});
JobTemplatesAPI.launch.mockResolvedValue({ JobTemplatesAPI.launch.mockResolvedValue({
data: { data: {
id: 9000, id: 9000,
@@ -51,10 +53,10 @@ describe('LaunchButton', () => {
expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1); expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
await sleep(0); await sleep(0);
expect(JobTemplatesAPI.launch).toHaveBeenCalledWith(1); expect(JobTemplatesAPI.launch).toHaveBeenCalledWith(1);
expect(history.push).toHaveBeenCalledWith('/jobs/9000'); expect(history.location.pathname).toEqual('/jobs/9000');
done();
}); });
test('displays error modal after unsuccessful launch', async done => {
test('displays error modal after unsuccessful launch', async () => {
JobTemplatesAPI.launch.mockRejectedValue( JobTemplatesAPI.launch.mockRejectedValue(
new Error({ new Error({
response: { response: {
@@ -79,6 +81,5 @@ describe('LaunchButton', () => {
await sleep(0); await sleep(0);
wrapper.update(); wrapper.update();
expect(wrapper.find('Modal').length).toBe(0); expect(wrapper.find('Modal').length).toBe(0);
done();
}); });
}); });

View File

@@ -81,7 +81,9 @@ describe('<NotificationList />', () => {
); );
await sleep(0); await sleep(0);
wrapper.update(); wrapper.update();
expect(wrapper).toMatchSnapshot(); const dataList = wrapper.find('PaginatedDataList');
expect(dataList).toHaveLength(1);
expect(dataList.prop('items')).toEqual(data.results);
}); });
test('should render list fetched of items', async () => { test('should render list fetched of items', async () => {

View File

@@ -159,9 +159,27 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
}, },
"displayName": "Styled(Link)", "displayName": "Styled(Link)",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"propTypes": Object {
"innerRef": [Function],
"onClick": [Function],
"replace": [Function],
"target": [Function],
"to": [Function],
},
"render": [Function], "render": [Function],
"styledComponentId": "sc-bdVaJa", "styledComponentId": "sc-bdVaJa",
"target": [Function], "target": Object {
"$$typeof": Symbol(react.forward_ref),
"displayName": "Link",
"propTypes": Object {
"innerRef": [Function],
"onClick": [Function],
"replace": [Function],
"target": [Function],
"to": [Function],
},
"render": [Function],
},
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
"withComponent": [Function], "withComponent": [Function],
@@ -176,23 +194,29 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
> >
<Link <Link
className="sc-bdVaJa eBseNd" className="sc-bdVaJa eBseNd"
replace={false}
to={ to={
Object { Object {
"pathname": "/foo", "pathname": "/foo",
} }
} }
> >
<a <LinkAnchor
className="sc-bdVaJa eBseNd" className="sc-bdVaJa eBseNd"
onClick={[Function]} href="/foo"
navigate={[Function]}
> >
<b <a
id="items-list-item-9000" className="sc-bdVaJa eBseNd"
href="/foo"
onClick={[Function]}
> >
Foo <b
</b> id="items-list-item-9000"
</a> >
Foo
</b>
</a>
</LinkAnchor>
</Link> </Link>
</StyledComponent> </StyledComponent>
</Styled(Link)> </Styled(Link)>

View File

@@ -43,7 +43,6 @@ export function main(render) {
const el = document.getElementById('app'); const el = document.getElementById('app');
document.title = `Ansible ${BrandName}`; document.title = `Ansible ${BrandName}`;
const defaultRedirect = () => <Redirect to="/home" />;
const removeTrailingSlash = ( const removeTrailingSlash = (
<Route <Route
exact exact
@@ -56,31 +55,38 @@ export function main(render) {
}) => <Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />} }) => <Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />}
/> />
); );
const loginRoutes = (
<Switch> const defaultRedirect = () => {
{removeTrailingSlash} if (isAuthenticated(document.cookie)) {
<Route return <Redirect to="/home" />;
path="/login" }
render={() => <Login isAuthenticated={isAuthenticated} />} return (
/> <Switch>
<Redirect to="/login" /> {removeTrailingSlash}
</Switch> <Route
); path="/login"
render={() => <Login isAuthenticated={isAuthenticated} />}
/>
<Redirect to="/login" />
</Switch>
);
};
return render( return render(
<RootProvider> <RootProvider>
<I18n> <I18n>
{({ i18n }) => ( {({ i18n }) => (
<Background> <Background>
{!isAuthenticated(document.cookie) ? ( <Switch>
loginRoutes {removeTrailingSlash}
) : ( <Route path="/login" render={defaultRedirect} />
<Switch> <Route exact path="/" render={defaultRedirect} />
{removeTrailingSlash} <Route
<Route path="/login" render={defaultRedirect} /> render={() => {
<Route exact path="/" render={defaultRedirect} /> if (!isAuthenticated(document.cookie)) {
<Route return <Redirect to="/login" />;
render={() => ( }
return (
<App <App
navLabel={i18n._(t`Primary Navigation`)} navLabel={i18n._(t`Primary Navigation`)}
routeGroups={[ routeGroups={[
@@ -250,10 +256,10 @@ export function main(render) {
return <Switch>{routeList}</Switch>; return <Switch>{routeList}</Switch>;
}} }}
/> />
)} );
/> }}
</Switch> />
)} </Switch>
</Background> </Background>
)} )}
</I18n> </I18n>

View File

@@ -82,12 +82,8 @@ describe('<JobDetail />', () => {
modal.find('button[aria-label="Delete"]').simulate('click'); modal.find('button[aria-label="Delete"]').simulate('click');
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1); expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
}); });
/*
The test below is skipped until react can be upgraded to at least 16.9.0. An upgrade to test('should display error modal when a job does not delete properly', async () => {
react - router will likely be necessary also.
See: https://github.com/ansible/awx/issues/4817
*/
test.skip('should display error modal when a job does not delete properly', async () => {
ProjectUpdatesAPI.destroy.mockRejectedValue( ProjectUpdatesAPI.destroy.mockRejectedValue(
new Error({ new Error({
response: { response: {
@@ -102,13 +98,11 @@ describe('<JobDetail />', () => {
); );
const wrapper = mountWithContexts(<JobDetail job={mockJobData} />); const wrapper = mountWithContexts(<JobDetail job={mockJobData} />);
wrapper wrapper.find('button[aria-label="Delete"]').simulate('click');
.find('button')
.at(0)
.simulate('click');
const modal = wrapper.find('Modal'); const modal = wrapper.find('Modal');
expect(modal.length).toBe(1);
await act(async () => { await act(async () => {
await modal.find('Button[variant="danger"]').prop('onClick')(); modal.find('button[aria-label="Delete"]').simulate('click');
}); });
wrapper.update(); wrapper.update();

View File

@@ -85,7 +85,7 @@ describe('<OrganizationAccess />', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} /> <OrganizationAccess organization={organization} />
); );
expect(wrapper.find('OrganizationAccess')).toMatchSnapshot(); expect(wrapper.find('PaginatedDataList')).toHaveLength(1);
}); });
test('should fetch and display access records on mount', async done => { test('should fetch and display access records on mount', async done => {

View File

@@ -273,9 +273,27 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
}, },
"displayName": "Styled(Link)", "displayName": "Styled(Link)",
"foldedComponentIds": Array [], "foldedComponentIds": Array [],
"propTypes": Object {
"innerRef": [Function],
"onClick": [Function],
"replace": [Function],
"target": [Function],
"to": [Function],
},
"render": [Function], "render": [Function],
"styledComponentId": "sc-bdVaJa", "styledComponentId": "sc-bdVaJa",
"target": [Function], "target": Object {
"$$typeof": Symbol(react.forward_ref),
"displayName": "Link",
"propTypes": Object {
"innerRef": [Function],
"onClick": [Function],
"replace": [Function],
"target": [Function],
"to": [Function],
},
"render": [Function],
},
"toString": [Function], "toString": [Function],
"warnTooManyClasses": [Function], "warnTooManyClasses": [Function],
"withComponent": [Function], "withComponent": [Function],
@@ -290,19 +308,25 @@ exports[`<OrganizationAccessItem /> initially renders succesfully 1`] = `
> >
<Link <Link
className="sc-bdVaJa fqQVUT" className="sc-bdVaJa fqQVUT"
replace={false}
to={ to={
Object { Object {
"pathname": "/bar", "pathname": "/bar",
} }
} }
> >
<a <LinkAnchor
className="sc-bdVaJa fqQVUT" className="sc-bdVaJa fqQVUT"
onClick={[Function]} href="/bar"
navigate={[Function]}
> >
jane <a
</a> className="sc-bdVaJa fqQVUT"
href="/bar"
onClick={[Function]}
>
jane
</a>
</LinkAnchor>
</Link> </Link>
</StyledComponent> </StyledComponent>
</Styled(Link)> </Styled(Link)>

View File

@@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import { createMemoryHistory } from 'history';
import { import {
mountWithContexts, mountWithContexts,
waitForElement, waitForElement,
} from '../../../../testUtils/enzymeHelpers'; } from '../../../../testUtils/enzymeHelpers';
import OrganizationAdd from './OrganizationAdd'; import OrganizationAdd from './OrganizationAdd';
import { OrganizationsAPI } from '../../../api'; import { OrganizationsAPI } from '../../../api';
@@ -27,33 +26,25 @@ describe('<OrganizationAdd />', () => {
}); });
test('should navigate to organizations list when cancel is clicked', () => { test('should navigate to organizations list when cancel is clicked', () => {
const history = { const history = createMemoryHistory({});
push: jest.fn(),
};
const wrapper = mountWithContexts(<OrganizationAdd />, { const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { router: { history } }, context: { router: { history } },
}); });
expect(history.push).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(history.push).toHaveBeenCalledWith('/organizations'); expect(history.location.pathname).toEqual('/organizations');
}); });
test('should navigate to organizations list when close (x) is clicked', () => { test('should navigate to organizations list when close (x) is clicked', () => {
const history = { const history = createMemoryHistory({});
push: jest.fn(),
};
const wrapper = mountWithContexts(<OrganizationAdd />, { const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { router: { history } }, context: { router: { history } },
}); });
expect(history.push).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Close"]').prop('onClick')(); wrapper.find('button[aria-label="Close"]').prop('onClick')();
expect(history.push).toHaveBeenCalledWith('/organizations'); expect(history.location.pathname).toEqual('/organizations');
}); });
test('successful form submission should trigger redirect', async done => { test('successful form submission should trigger redirect', async () => {
const history = { const history = createMemoryHistory({});
push: jest.fn(),
};
const orgData = { const orgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
@@ -77,11 +68,10 @@ describe('<OrganizationAdd />', () => {
[3], [3],
[] []
); );
expect(history.push).toHaveBeenCalledWith('/organizations/5'); expect(history.location.pathname).toEqual('/organizations/5');
done();
}); });
test('handleSubmit should post instance groups', async done => { test('handleSubmit should post instance groups', async () => {
const orgData = { const orgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
@@ -104,7 +94,6 @@ describe('<OrganizationAdd />', () => {
[] []
); );
expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(5, 3); expect(OrganizationsAPI.associateInstanceGroup).toHaveBeenCalledWith(5, 3);
done();
}); });
test('AnsibleSelect component renders if there are virtual environments', () => { test('AnsibleSelect component renders if there are virtual environments', () => {

View File

@@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { createMemoryHistory } from 'history';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import OrganizationEdit from './OrganizationEdit'; import OrganizationEdit from './OrganizationEdit';
jest.mock('@api'); jest.mock('@api');
@@ -65,17 +64,14 @@ describe('<OrganizationEdit />', () => {
}); });
test('should navigate to organization detail when cancel is clicked', () => { test('should navigate to organization detail when cancel is clicked', () => {
const history = { const history = createMemoryHistory({});
push: jest.fn(),
};
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<OrganizationEdit organization={mockData} />, <OrganizationEdit organization={mockData} />,
{ context: { router: { history } } } { context: { router: { history } } }
); );
expect(history.push).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(history.push).toHaveBeenCalledWith('/organizations/1/details'); expect(history.location.pathname).toEqual('/organizations/1/details');
}); });
}); });

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import ProjectDetail from './ProjectDetail'; import ProjectDetail from './ProjectDetail';
@@ -175,46 +176,26 @@ describe('<ProjectDetail />', () => {
}); });
test('edit button should navigate to project edit', () => { test('edit button should navigate to project edit', () => {
const context = { const history = createMemoryHistory();
router: {
history: {
push: jest.fn(),
replace: jest.fn(),
createHref: jest.fn(),
},
},
};
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, { const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, {
context, context: { router: { history } },
}); });
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1); expect(wrapper.find('Button[aria-label="edit"]').length).toBe(1);
expect(context.router.history.push).not.toHaveBeenCalled();
wrapper wrapper
.find('Button[aria-label="edit"] Link') .find('Button[aria-label="edit"] Link')
.simulate('click', { button: 0 }); .simulate('click', { button: 0 });
expect(context.router.history.push).toHaveBeenCalledWith( expect(history.location.pathname).toEqual('/projects/1/edit');
'/projects/1/edit'
);
}); });
test('close button should navigate to projects list', () => { test('close button should navigate to projects list', () => {
const context = { const history = createMemoryHistory();
router: {
history: {
push: jest.fn(),
replace: jest.fn(),
createHref: jest.fn(),
},
},
};
const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, { const wrapper = mountWithContexts(<ProjectDetail project={mockProject} />, {
context, context: { router: { history } },
}); });
expect(wrapper.find('Button[aria-label="close"]').length).toBe(1); expect(wrapper.find('Button[aria-label="close"]').length).toBe(1);
expect(context.router.history.push).not.toHaveBeenCalled();
wrapper wrapper
.find('Button[aria-label="close"] Link') .find('Button[aria-label="close"] Link')
.simulate('click', { button: 0 }); .simulate('click', { button: 0 });
expect(context.router.history.push).toHaveBeenCalledWith('/projects'); expect(history.location.pathname).toEqual('/projects');
}); });
}); });

View File

@@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils'; import { sleep } from '@testUtils/testUtils';
import JobTemplateAdd from './JobTemplateAdd'; import JobTemplateAdd from './JobTemplateAdd';
@@ -27,8 +29,6 @@ const jobTemplateData = {
host_config_key: '', host_config_key: '',
}; };
// TODO: Needs React/React-router upgrade to remove `act()` warnings
// See https://github.com/ansible/awx/issues/4817
describe('<JobTemplateAdd />', () => { describe('<JobTemplateAdd />', () => {
const defaultProps = { const defaultProps = {
description: '', description: '',
@@ -52,13 +52,19 @@ describe('<JobTemplateAdd />', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('should render Job Template Form', () => { test('should render Job Template Form', async () => {
const wrapper = mountWithContexts(<JobTemplateAdd />); let wrapper;
await act(async () => {
wrapper = mountWithContexts(<JobTemplateAdd />);
});
expect(wrapper.find('JobTemplateForm').length).toBe(1); expect(wrapper.find('JobTemplateForm').length).toBe(1);
}); });
test('should render Job Template Form with default values', async () => { test('should render Job Template Form with default values', async () => {
const wrapper = mountWithContexts(<JobTemplateAdd />); let wrapper;
await act(async () => {
wrapper = mountWithContexts(<JobTemplateAdd />);
});
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
expect(wrapper.find('input#template-description').text()).toBe( expect(wrapper.find('input#template-description').text()).toBe(
defaultProps.description defaultProps.description
@@ -89,7 +95,10 @@ describe('<JobTemplateAdd />', () => {
...jobTemplateData, ...jobTemplateData,
}, },
}); });
const wrapper = mountWithContexts(<JobTemplateAdd />); let wrapper;
await act(async () => {
wrapper = mountWithContexts(<JobTemplateAdd />);
});
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
const formik = wrapper.find('Formik').instance(); const formik = wrapper.find('Formik').instance();
const changeState = new Promise(resolve => { const changeState = new Promise(resolve => {
@@ -98,6 +107,7 @@ describe('<JobTemplateAdd />', () => {
values: { values: {
...jobTemplateData, ...jobTemplateData,
labels: [], labels: [],
instanceGroups: [],
}, },
}, },
() => resolve() () => resolve()
@@ -110,9 +120,7 @@ describe('<JobTemplateAdd />', () => {
}); });
test('should navigate to job template detail after form submission', async () => { test('should navigate to job template detail after form submission', async () => {
const history = { const history = createMemoryHistory({});
push: jest.fn(),
};
JobTemplatesAPI.create.mockResolvedValueOnce({ JobTemplatesAPI.create.mockResolvedValueOnce({
data: { data: {
id: 1, id: 1,
@@ -120,28 +128,57 @@ describe('<JobTemplateAdd />', () => {
...jobTemplateData, ...jobTemplateData,
}, },
}); });
const wrapper = mountWithContexts(<JobTemplateAdd />, { let wrapper;
context: { router: { history } }, await act(async () => {
wrapper = mountWithContexts(<JobTemplateAdd />, {
context: { router: { history } },
});
}); });
const updatedTemplateData = {
name: 'new name',
description: 'new description',
job_type: 'check',
};
const labels = [
{ id: 3, name: 'Foo', isNew: true },
{ id: 4, name: 'Bar', isNew: true },
{ id: 5, name: 'Maple' },
{ id: 6, name: 'Tree' },
];
JobTemplatesAPI.update.mockResolvedValue({
data: { ...updatedTemplateData },
});
const formik = wrapper.find('Formik').instance();
const changeState = new Promise(resolve => {
const values = {
...jobTemplateData,
...updatedTemplateData,
labels,
instanceGroups: [],
};
formik.setState({ values }, () => resolve());
});
await changeState;
await wrapper.find('JobTemplateForm').invoke('handleSubmit')( await wrapper.find('JobTemplateForm').invoke('handleSubmit')(
jobTemplateData jobTemplateData
); );
await sleep(0); await sleep(0);
expect(history.push).toHaveBeenCalledWith( expect(history.location.pathname).toEqual(
'/templates/job_template/1/details' '/templates/job_template/1/details'
); );
}); });
test('should navigate to templates list when cancel is clicked', async () => { test('should navigate to templates list when cancel is clicked', async () => {
const history = { const history = createMemoryHistory({});
push: jest.fn(), let wrapper;
}; await act(async () => {
const wrapper = mountWithContexts(<JobTemplateAdd />, { wrapper = mountWithContexts(<JobTemplateAdd />, {
context: { router: { history } }, context: { router: { history } },
});
}); });
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
expect(history.push).toHaveBeenCalledWith('/templates'); expect(history.location.pathname).toEqual('/templates');
}); });
}); });

View File

@@ -50,14 +50,7 @@ describe('<JobTemplateDetail />', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('initially renders succesfully', () => { test('Can load with missing summary fields', async () => {
const wrapper = mountWithContexts(
<JobTemplateDetail template={template} />
);
expect(wrapper).toMatchSnapshot();
});
test('Can load with missing summary fields', async done => {
const mockTemplate = { ...template }; const mockTemplate = { ...template };
mockTemplate.summary_fields = { user_capabilities: {} }; mockTemplate.summary_fields = { user_capabilities: {} };
@@ -69,8 +62,8 @@ describe('<JobTemplateDetail />', () => {
'Detail[label="Description"]', 'Detail[label="Description"]',
el => el.length === 1 el => el.length === 1
); );
done();
}); });
test('When component mounts API is called to get instance groups', async done => { test('When component mounts API is called to get instance groups', async done => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<JobTemplateDetail template={template} /> <JobTemplateDetail template={template} />
@@ -89,6 +82,7 @@ describe('<JobTemplateDetail />', () => {
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1); expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
done(); done();
}); });
test('Edit button is absent when user does not have edit privilege', async done => { test('Edit button is absent when user does not have edit privilege', async done => {
const regularUser = { const regularUser = {
forks: 1, forks: 1,

View File

@@ -1,194 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<JobTemplateDetail /> initially renders succesfully 1`] = `
<Wrap>
<WithI18n
template={
Object {
"forks": 1,
"host_config_key": "ssh",
"id": 1,
"inventory": 1,
"job_type": "run",
"limit": "1",
"name": "Temp 1",
"playbook": "",
"project": 7,
"summary_fields": Object {
"created_by": Object {
"username": "Joe",
},
"credentials": Array [
Object {
"id": 1,
"kind": "ssh",
"name": "Credential 1",
},
Object {
"id": 2,
"kind": "awx",
"name": "Credential 2",
},
],
"inventory": Object {
"name": "Inventory",
},
"modified_by": Object {
"username": "Joe",
},
"project": Object {
"name": "Project",
},
"user_capabilities": Object {
"edit": true,
},
},
"verbosity": 1,
}
}
>
<I18n
update={true}
withHash={true}
>
<withRouter(JobTemplateDetail)
i18n={"/i18n/"}
template={
Object {
"forks": 1,
"host_config_key": "ssh",
"id": 1,
"inventory": 1,
"job_type": "run",
"limit": "1",
"name": "Temp 1",
"playbook": "",
"project": 7,
"summary_fields": Object {
"created_by": Object {
"username": "Joe",
},
"credentials": Array [
Object {
"id": 1,
"kind": "ssh",
"name": "Credential 1",
},
Object {
"id": 2,
"kind": "awx",
"name": "Credential 2",
},
],
"inventory": Object {
"name": "Inventory",
},
"modified_by": Object {
"username": "Joe",
},
"project": Object {
"name": "Project",
},
"user_capabilities": Object {
"edit": true,
},
},
"verbosity": 1,
}
}
>
<Route>
<JobTemplateDetail
history={"/history/"}
i18n={"/i18n/"}
location={
Object {
"hash": "",
"pathname": "",
"search": "",
"state": "",
}
}
match={
Object {
"isExact": false,
"params": Object {},
"path": "",
"url": "",
}
}
template={
Object {
"forks": 1,
"host_config_key": "ssh",
"id": 1,
"inventory": 1,
"job_type": "run",
"limit": "1",
"name": "Temp 1",
"playbook": "",
"project": 7,
"summary_fields": Object {
"created_by": Object {
"username": "Joe",
},
"credentials": Array [
Object {
"id": 1,
"kind": "ssh",
"name": "Credential 1",
},
Object {
"id": 2,
"kind": "awx",
"name": "Credential 2",
},
],
"inventory": Object {
"name": "Inventory",
},
"modified_by": Object {
"username": "Joe",
},
"project": Object {
"name": "Project",
},
"user_capabilities": Object {
"edit": true,
},
},
"verbosity": 1,
}
}
>
<WithI18n>
<I18n
update={true}
withHash={true}
>
<ContentLoading
i18n={"/i18n/"}
>
<EmptyState>
<div
className="pf-c-empty-state styles.modifiers.lg"
>
<EmptyStateBody>
<div
className="pf-c-empty-state__body"
>
Loading...
</div>
</EmptyStateBody>
</div>
</EmptyState>
</ContentLoading>
</I18n>
</WithI18n>
</JobTemplateDetail>
</Route>
</withRouter(JobTemplateDetail)>
</I18n>
</WithI18n>
</Wrap>
`;

View File

@@ -1,4 +1,6 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { sleep } from '@testUtils/testUtils'; import { sleep } from '@testUtils/testUtils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { JobTemplatesAPI, LabelsAPI, ProjectsAPI } from '@api'; import { JobTemplatesAPI, LabelsAPI, ProjectsAPI } from '@api';
@@ -35,6 +37,7 @@ const mockJobTemplate = {
results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }], results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }],
}, },
inventory: { inventory: {
id: 2,
organization_id: 1, organization_id: 1,
}, },
}, },
@@ -157,16 +160,22 @@ describe('<JobTemplateEdit />', () => {
}); });
test('initially renders successfully', async () => { test('initially renders successfully', async () => {
const wrapper = mountWithContexts( let wrapper;
<JobTemplateEdit template={mockJobTemplate} /> await act(async () => {
); wrapper = mountWithContexts(
<JobTemplateEdit template={mockJobTemplate} />
);
});
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
}); });
test('handleSubmit should call api update', async done => { test('handleSubmit should call api update', async () => {
const wrapper = mountWithContexts( let wrapper;
<JobTemplateEdit template={mockJobTemplate} /> await act(async () => {
); wrapper = mountWithContexts(
<JobTemplateEdit template={mockJobTemplate} />
);
});
await waitForElement(wrapper, 'JobTemplateForm', e => e.length === 1); await waitForElement(wrapper, 'JobTemplateForm', e => e.length === 1);
const updatedTemplateData = { const updatedTemplateData = {
name: 'new name', name: 'new name',
@@ -184,16 +193,13 @@ describe('<JobTemplateEdit />', () => {
}); });
const formik = wrapper.find('Formik').instance(); const formik = wrapper.find('Formik').instance();
const changeState = new Promise(resolve => { const changeState = new Promise(resolve => {
formik.setState( const values = {
{ ...mockJobTemplate,
values: { ...updatedTemplateData,
...mockJobTemplate, labels,
...updatedTemplateData, instanceGroups: [],
labels, };
}, formik.setState({ values }, () => resolve());
},
() => resolve()
);
}); });
await changeState; await changeState;
wrapper.find('button[aria-label="Save"]').simulate('click'); wrapper.find('button[aria-label="Save"]').simulate('click');
@@ -206,25 +212,25 @@ describe('<JobTemplateEdit />', () => {
expect(JobTemplatesAPI.disassociateLabel).toHaveBeenCalledTimes(2); expect(JobTemplatesAPI.disassociateLabel).toHaveBeenCalledTimes(2);
expect(JobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(2); expect(JobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(2);
expect(JobTemplatesAPI.generateLabel).toHaveBeenCalledTimes(2); expect(JobTemplatesAPI.generateLabel).toHaveBeenCalledTimes(2);
done();
}); });
test('should navigate to job template detail when cancel is clicked', async done => { test('should navigate to job template detail when cancel is clicked', async () => {
const history = { push: jest.fn() }; const history = createMemoryHistory({});
const wrapper = mountWithContexts( let wrapper;
<JobTemplateEdit template={mockJobTemplate} />, await act(async () => {
{ context: { router: { history } } } wrapper = mountWithContexts(
); <JobTemplateEdit template={mockJobTemplate} />,
{ context: { router: { history } } }
);
});
const cancelButton = await waitForElement( const cancelButton = await waitForElement(
wrapper, wrapper,
'button[aria-label="Cancel"]', 'button[aria-label="Cancel"]',
e => e.length === 1 e => e.length === 1
); );
expect(history.push).not.toHaveBeenCalled();
cancelButton.prop('onClick')(); cancelButton.prop('onClick')();
expect(history.push).toHaveBeenCalledWith( expect(history.location.pathname).toEqual(
'/templates/job_template/1/details' '/templates/job_template/1/details'
); );
done();
}); });
}); });

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils'; import { sleep } from '@testUtils/testUtils';
import JobTemplateForm from './JobTemplateForm'; import JobTemplateForm from './JobTemplateForm';
@@ -71,14 +72,16 @@ describe('<JobTemplateForm />', () => {
}); });
test('should render LabelsSelect', async () => { test('should render LabelsSelect', async () => {
const wrapper = mountWithContexts( let wrapper;
<JobTemplateForm await act(async () => {
template={mockData} wrapper = mountWithContexts(
handleSubmit={jest.fn()} <JobTemplateForm
handleCancel={jest.fn()} template={mockData}
/> handleSubmit={jest.fn()}
); handleCancel={jest.fn()}
await waitForElement(wrapper, 'Form', el => el.length === 0); />
);
});
expect(LabelsAPI.read).toHaveBeenCalled(); expect(LabelsAPI.read).toHaveBeenCalled();
expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalled(); expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalled();
wrapper.update(); wrapper.update();
@@ -90,13 +93,16 @@ describe('<JobTemplateForm />', () => {
}); });
test('should update form values on input changes', async () => { test('should update form values on input changes', async () => {
const wrapper = mountWithContexts( let wrapper;
<JobTemplateForm await act(async () => {
template={mockData} wrapper = mountWithContexts(
handleSubmit={jest.fn()} <JobTemplateForm
handleCancel={jest.fn()} template={mockData}
/> handleSubmit={jest.fn()}
); handleCancel={jest.fn()}
/>
);
});
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
const form = wrapper.find('Formik'); const form = wrapper.find('Formik');
@@ -112,14 +118,16 @@ describe('<JobTemplateForm />', () => {
target: { value: 'new job type', name: 'job_type' }, target: { value: 'new job type', name: 'job_type' },
}); });
expect(form.state('values').job_type).toEqual('new job type'); expect(form.state('values').job_type).toEqual('new job type');
wrapper.find('InventoryLookup').prop('onChange')({ wrapper.find('InventoryLookup').invoke('onChange')({
id: 3, id: 3,
name: 'inventory', name: 'inventory',
}); });
expect(form.state('values').inventory).toEqual(3); expect(form.state('values').inventory).toEqual(3);
wrapper.find('ProjectLookup').prop('onChange')({ await act(async () => {
id: 4, wrapper.find('ProjectLookup').invoke('onChange')({
name: 'project', id: 4,
name: 'project',
});
}); });
expect(form.state('values').project).toEqual(4); expect(form.state('values').project).toEqual(4);
wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', { wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', {
@@ -130,13 +138,16 @@ describe('<JobTemplateForm />', () => {
test('should call handleSubmit when Submit button is clicked', async () => { test('should call handleSubmit when Submit button is clicked', async () => {
const handleSubmit = jest.fn(); const handleSubmit = jest.fn();
const wrapper = mountWithContexts( let wrapper;
<JobTemplateForm await act(async () => {
template={mockData} wrapper = mountWithContexts(
handleSubmit={handleSubmit} <JobTemplateForm
handleCancel={jest.fn()} template={mockData}
/> handleSubmit={handleSubmit}
); handleCancel={jest.fn()}
/>
);
});
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
expect(handleSubmit).not.toHaveBeenCalled(); expect(handleSubmit).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Save"]').simulate('click'); wrapper.find('button[aria-label="Save"]').simulate('click');
@@ -146,16 +157,19 @@ describe('<JobTemplateForm />', () => {
test('should call handleCancel when Cancel button is clicked', async () => { test('should call handleCancel when Cancel button is clicked', async () => {
const handleCancel = jest.fn(); const handleCancel = jest.fn();
const wrapper = mountWithContexts( let wrapper;
<JobTemplateForm await act(async () => {
template={mockData} wrapper = mountWithContexts(
handleSubmit={jest.fn()} <JobTemplateForm
handleCancel={handleCancel} template={mockData}
/> handleSubmit={jest.fn()}
); handleCancel={handleCancel}
/>
);
});
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
expect(handleCancel).not.toHaveBeenCalled(); expect(handleCancel).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
expect(handleCancel).toBeCalled(); expect(handleCancel).toBeCalled();
}); });
}); });

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { LabelsAPI } from '@api'; import { LabelsAPI } from '@api';
import { sleep } from '@testUtils/testUtils';
import LabelSelect from './LabelSelect'; import LabelSelect from './LabelSelect';
jest.mock('@api'); jest.mock('@api');
@@ -17,8 +17,10 @@ describe('<LabelSelect />', () => {
LabelsAPI.read.mockReturnValue({ LabelsAPI.read.mockReturnValue({
data: { results: options }, data: { results: options },
}); });
const wrapper = mount(<LabelSelect value={[]} />); let wrapper;
await sleep(1); await act(async () => {
wrapper = mount(<LabelSelect value={[]} onError={() => {}} />);
});
wrapper.update(); wrapper.update();
expect(LabelsAPI.read).toHaveBeenCalledTimes(1); expect(LabelsAPI.read).toHaveBeenCalledTimes(1);
@@ -37,8 +39,10 @@ describe('<LabelSelect />', () => {
results: options, results: options,
}, },
}); });
const wrapper = mount(<LabelSelect value={[]} />); let wrapper;
await sleep(1); await act(async () => {
wrapper = mount(<LabelSelect value={[]} onError={() => {}} />);
});
wrapper.update(); wrapper.update();
expect(LabelsAPI.read).toHaveBeenCalledTimes(2); expect(LabelsAPI.read).toHaveBeenCalledTimes(2);

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import PlaybookSelect from './PlaybookSelect'; import PlaybookSelect from './PlaybookSelect';
import { ProjectsAPI } from '@api'; import { ProjectsAPI } from '@api';
@@ -16,19 +17,24 @@ describe('<PlaybookSelect />', () => {
jest.resetAllMocks(); jest.resetAllMocks();
}); });
test('should reload playbooks when project value changes', () => { test('should reload playbooks when project value changes', async () => {
const wrapper = mountWithContexts( let wrapper;
<PlaybookSelect await act(async () => {
projectId={1} wrapper = mountWithContexts(
isValid <PlaybookSelect
form={{}} projectId={1}
field={{}} isValid
onError={() => {}} form={{}}
/> field={{ onChange: () => {}, value: '' }}
); onError={() => {}}
/>
);
});
expect(ProjectsAPI.readPlaybooks).toHaveBeenCalledWith(1); expect(ProjectsAPI.readPlaybooks).toHaveBeenCalledWith(1);
wrapper.setProps({ projectId: 15 }); await act(async () => {
wrapper.setProps({ projectId: 15 });
});
expect(ProjectsAPI.readPlaybooks).toHaveBeenCalledTimes(2); expect(ProjectsAPI.readPlaybooks).toHaveBeenCalledTimes(2);
expect(ProjectsAPI.readPlaybooks).toHaveBeenCalledWith(15); expect(ProjectsAPI.readPlaybooks).toHaveBeenCalledWith(15);

View File

@@ -45,14 +45,19 @@ exports[`mountWithContexts injected I18nProvider should mount and render deeply
exports[`mountWithContexts injected Router should mount and render 1`] = ` exports[`mountWithContexts injected Router should mount and render 1`] = `
<div> <div>
<Link <Link
replace={false}
to="/" to="/"
> >
<a <LinkAnchor
onClick={[Function]} href="/"
navigate={[Function]}
> >
home <a
</a> href="/"
onClick={[Function]}
>
home
</a>
</LinkAnchor>
</Link> </Link>
</div> </div>
`; `;

View File

@@ -5,6 +5,7 @@
import React from 'react'; import React from 'react';
import { shape, object, string, arrayOf } from 'prop-types'; import { shape, object, string, arrayOf } from 'prop-types';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { MemoryRouter, Router } from 'react-router-dom';
import { I18nProvider } from '@lingui/react'; import { I18nProvider } from '@lingui/react';
import { ConfigProvider } from '../src/contexts/Config'; import { ConfigProvider } from '../src/contexts/Config';
@@ -13,13 +14,13 @@ const intlProvider = new I18nProvider(
{ {
language, language,
catalogs: { catalogs: {
[language]: {} [language]: {},
} },
}, },
{} {}
); );
const { const {
linguiPublisher: { i18n: originalI18n } linguiPublisher: { i18n: originalI18n },
} = intlProvider.getChildContext(); } = intlProvider.getChildContext();
const defaultContexts = { const defaultContexts = {
@@ -34,13 +35,14 @@ const defaultContexts = {
ansible_version: null, ansible_version: null,
custom_virtualenvs: [], custom_virtualenvs: [],
version: null, version: null,
toJSON: () => '/config/' toJSON: () => '/config/',
}, },
router: { router: {
history: { history_: {
push: () => {}, push: () => {},
replace: () => {}, replace: () => {},
createHref: () => {}, createHref: () => {},
listen: () => {},
location: { location: {
hash: '', hash: '',
pathname: '', pathname: '',
@@ -61,33 +63,38 @@ const defaultContexts = {
isExact: false, isExact: false,
path: '', path: '',
url: '', url: '',
} },
}, },
toJSON: () => '/router/', toJSON: () => '/router/',
}, },
}; };
function wrapContexts (node, context) { function wrapContexts(node, context) {
const { config } = context; const { config, router } = context;
class Wrap extends React.Component { class Wrap extends React.Component {
render () { render() {
// eslint-disable-next-line react/no-this-in-sfc // eslint-disable-next-line react/no-this-in-sfc
const { children, ...props } = this.props; const { children, ...props } = this.props;
const component = React.cloneElement(children, props); const component = React.cloneElement(children, props);
if (router.history) {
return (
<ConfigProvider value={config}>
<Router history={router.history}>{component}</Router>
</ConfigProvider>
);
}
return ( return (
<ConfigProvider value={config}> <ConfigProvider value={config}>
{component} <MemoryRouter>{component}</MemoryRouter>
</ConfigProvider> </ConfigProvider>
); );
} }
} }
return ( return <Wrap>{node}</Wrap>;
<Wrap>{node}</Wrap>
);
} }
function applyDefaultContexts (context) { function applyDefaultContexts(context) {
if (!context) { if (!context) {
return defaultContexts; return defaultContexts;
} }
@@ -101,16 +108,16 @@ function applyDefaultContexts (context) {
return newContext; return newContext;
} }
export function shallowWithContexts (node, options = {}) { export function shallowWithContexts(node, options = {}) {
const context = applyDefaultContexts(options.context); const context = applyDefaultContexts(options.context);
return shallow(wrapContexts(node, context)); return shallow(wrapContexts(node, context));
} }
export function mountWithContexts (node, options = {}) { export function mountWithContexts(node, options = {}) {
const context = applyDefaultContexts(options.context); const context = applyDefaultContexts(options.context);
const childContextTypes = { const childContextTypes = {
linguiPublisher: shape({ linguiPublisher: shape({
i18n: object.isRequired i18n: object.isRequired,
}).isRequired, }).isRequired,
config: shape({ config: shape({
ansible_version: string, ansible_version: string,
@@ -122,9 +129,9 @@ export function mountWithContexts (node, options = {}) {
location: shape({}), location: shape({}),
match: shape({}), match: shape({}),
}).isRequired, }).isRequired,
history: shape({}).isRequired, history: shape({}),
}), }),
...options.childContextTypes ...options.childContextTypes,
}; };
return mount(wrapContexts(node, context), { context, childContextTypes }); return mount(wrapContexts(node, context), { context, childContextTypes });
} }
@@ -136,11 +143,15 @@ export function mountWithContexts (node, options = {}) {
* @param[selector] - The selector of the element(s) to wait for. * @param[selector] - The selector of the element(s) to wait for.
* @param[callback] - Callback to poll - by default this checks for a node count of 1. * @param[callback] - Callback to poll - by default this checks for a node count of 1.
*/ */
export function waitForElement (wrapper, selector, callback = el => el.length === 1) { export function waitForElement(
wrapper,
selector,
callback = el => el.length === 1
) {
const interval = 100; const interval = 100;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let attempts = 30; let attempts = 30;
(function pollElement () { (function pollElement() {
wrapper.update(); wrapper.update();
const el = wrapper.find(selector); const el = wrapper.find(selector);
if (callback(el)) { if (callback(el)) {
@@ -151,6 +162,6 @@ export function waitForElement (wrapper, selector, callback = el => el.length ==
return reject(new Error(message)); return reject(new Error(message));
} }
return setTimeout(pollElement, interval); return setTimeout(pollElement, interval);
}()); })();
}); });
} }

View File

@@ -1,4 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { createMemoryHistory } from 'history';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
@@ -21,7 +22,7 @@ describe('mountWithContexts', () => {
const Child = withI18n()(({ i18n }) => ( const Child = withI18n()(({ i18n }) => (
<div>{i18n._(t`Text content`)}</div> <div>{i18n._(t`Text content`)}</div>
)); ));
const Parent = () => (<Child />); const Parent = () => <Child />;
const wrapper = mountWithContexts(<Parent />); const wrapper = mountWithContexts(<Parent />);
expect(wrapper.find('Parent')).toMatchSnapshot(); expect(wrapper.find('Parent')).toMatchSnapshot();
}); });
@@ -41,23 +42,17 @@ describe('mountWithContexts', () => {
it('should mount and render with stubbed context', () => { it('should mount and render with stubbed context', () => {
const context = { const context = {
router: { router: {
history: { history: createMemoryHistory({}),
push: jest.fn(),
replace: jest.fn(),
createHref: jest.fn(),
},
route: { route: {
location: {}, location: {},
match: {} match: {},
} },
} },
}; };
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
( <div>
<div> <Link to="/">home</Link>
<Link to="/">home</Link> </div>,
</div>
),
{ context } { context }
); );
@@ -66,7 +61,7 @@ describe('mountWithContexts', () => {
link.simulate('click', { button: 0 }); link.simulate('click', { button: 0 });
wrapper.update(); wrapper.update();
expect(context.router.history.push).toHaveBeenCalledWith('/'); expect(context.router.history.location.pathname).toEqual('/');
}); });
}); });
@@ -101,10 +96,7 @@ describe('mountWithContexts', () => {
)} )}
</Config> </Config>
); );
const wrapper = mountWithContexts( const wrapper = mountWithContexts(<Foo />, { context: { config } });
<Foo />,
{ context: { config } }
);
expect(wrapper.find('Foo')).toMatchSnapshot(); expect(wrapper.find('Foo')).toMatchSnapshot();
}); });
}); });
@@ -115,26 +107,26 @@ describe('mountWithContexts', () => {
* after a short amount of time. * after a short amount of time.
*/ */
class TestAsyncComponent extends Component { class TestAsyncComponent extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { displayElement: false }; this.state = { displayElement: false };
} }
componentDidMount () { componentDidMount() {
setTimeout(() => this.setState({ displayElement: true }), 500); setTimeout(() => this.setState({ displayElement: true }), 500);
} }
render () { render() {
const { displayElement } = this.state; const { displayElement } = this.state;
if (displayElement) { if (displayElement) {
return (<div id="test-async-component" />); return <div id="test-async-component" />;
} }
return null; return null;
} }
} }
describe('waitForElement', () => { describe('waitForElement', () => {
it('waits for the element and returns it', async (done) => { it('waits for the element and returns it', async done => {
const selector = '#test-async-component'; const selector = '#test-async-component';
const wrapper = mountWithContexts(<TestAsyncComponent />); const wrapper = mountWithContexts(<TestAsyncComponent />);
expect(wrapper.exists(selector)).toEqual(false); expect(wrapper.exists(selector)).toEqual(false);
@@ -145,7 +137,7 @@ describe('waitForElement', () => {
done(); done();
}); });
it('eventually throws an error for elements that don\'t exist', async (done) => { it("eventually throws an error for elements that don't exist", async done => {
const wrapper = mountWithContexts(<div />); const wrapper = mountWithContexts(<div />);
let error; let error;
@@ -154,7 +146,11 @@ describe('waitForElement', () => {
} catch (err) { } catch (err) {
error = err; error = err;
} finally { } finally {
expect(error).toEqual(new Error('Expected condition for <#does-not-exist> not met: el => el.length === 1')); expect(error).toEqual(
new Error(
'Expected condition for <#does-not-exist> not met: el => el.length === 1'
)
);
done(); done();
} }
}); });