update content loading and error handling

unwind error handling

use auth cookie as source of truth, fetch config only when authenticated
This commit is contained in:
Jake McDermott
2019-05-09 15:59:43 -04:00
parent 534418c81a
commit e72f0bcfd4
50 changed files with 4721 additions and 4724 deletions

View File

@@ -1,16 +1,33 @@
import React from 'react';
import { mountWithContexts, waitForElement } from './enzymeHelpers';
import { asyncFlush } from '../jest.setup';
import App from '../src/App';
import { RootAPI } from '../src/api';
import { ConfigAPI, MeAPI, RootAPI } from '../src/api';
jest.mock('../src/api');
describe('<App />', () => {
const ansible_version = '111';
const custom_virtualenvs = [];
const version = '222';
beforeEach(() => {
ConfigAPI.read = () => Promise.resolve({
data: {
ansible_version,
custom_virtualenvs,
version
}
});
MeAPI.read = () => Promise.resolve({ data: { results: [{}] } });
});
afterEach(() => {
jest.clearAllMocks();
});
test('expected content is rendered', () => {
const appWrapper = mountWithContexts(
<App
@@ -34,7 +51,7 @@ describe('<App />', () => {
render={({ routeGroups }) => (
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
)}
/>
/>,
);
// page components
@@ -54,12 +71,8 @@ describe('<App />', () => {
});
test('opening the about modal renders prefetched config data', async (done) => {
const ansible_version = '111';
const version = '222';
const config = { ansible_version, version };
const wrapper = mountWithContexts(<App />, { context: { config } });
const wrapper = mountWithContexts(<App />);
wrapper.update();
// open about modal
const aboutDropdown = 'Dropdown QuestionCircleIcon';
@@ -67,9 +80,11 @@ describe('<App />', () => {
const aboutModalContent = 'AboutModalBoxContent';
const aboutModalClose = 'button[aria-label="Close Dialog"]';
expect(wrapper.find(aboutModalContent)).toHaveLength(0);
await waitForElement(wrapper, aboutDropdown);
wrapper.find(aboutDropdown).simulate('click');
wrapper.find(aboutButton).simulate('click');
const button = await waitForElement(wrapper, aboutButton, el => !el.props().disabled);
button.simulate('click');
// check about modal content
const content = await waitForElement(wrapper, aboutModalContent);
@@ -83,24 +98,21 @@ describe('<App />', () => {
done();
});
test('onNavToggle sets state.isNavOpen to opposite', () => {
test('handleNavToggle sets state.isNavOpen to opposite', () => {
const appWrapper = mountWithContexts(<App />).find('App');
const { onNavToggle } = appWrapper.instance();
const { handleNavToggle } = appWrapper.instance();
[true, false, true, false, true].forEach(expected => {
expect(appWrapper.state().isNavOpen).toBe(expected);
onNavToggle();
handleNavToggle();
});
});
test('onLogout makes expected call to api client', async (done) => {
const appWrapper = mountWithContexts(<App />, {
context: { network: { handleHttpError: () => {} } }
}).find('App');
appWrapper.instance().onLogout();
const appWrapper = mountWithContexts(<App />).find('App');
appWrapper.instance().handleLogout();
await asyncFlush();
expect(RootAPI.logout).toHaveBeenCalledTimes(1);
done();
});
});

View File

@@ -2,20 +2,16 @@
exports[`mountWithContexts injected ConfigProvider should mount and render with custom Config value 1`] = `
<Foo>
<Config>
<div>
Fizz
1.1
</div>
</Config>
<div>
Fizz
1.1
</div>
</Foo>
`;
exports[`mountWithContexts injected ConfigProvider should mount and render with default values 1`] = `
<Foo>
<Config>
<div />
</Config>
<div />
</Foo>
`;

View File

@@ -19,7 +19,6 @@ describe('<Lookup />', () => {
getItems={() => { }}
columns={mockColumns}
sortedColumnKey="name"
handleHttpError={() => {}}
/>
);
});
@@ -34,7 +33,6 @@ describe('<Lookup />', () => {
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
columns={mockColumns}
sortedColumnKey="name"
handleHttpError={() => {}}
/>
).find('Lookup');
@@ -57,7 +55,6 @@ describe('<Lookup />', () => {
getItems={() => { }}
columns={mockColumns}
sortedColumnKey="name"
handleHttpError={() => {}}
/>
).find('Lookup');
expect(spy).not.toHaveBeenCalled();

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { mountWithContexts } from '../enzymeHelpers';
import { _NotifyAndRedirect } from '../../src/components/NotifyAndRedirect';
describe('<NotifyAndRedirect />', () => {
test('initially renders succesfully and calls setRootDialogMessage', () => {
const setRootDialogMessage = jest.fn();
mountWithContexts(
<_NotifyAndRedirect
to="foo"
setRootDialogMessage={setRootDialogMessage}
location={{ pathname: 'foo' }}
i18n={{ _: val => val.toString() }}
/>
);
expect(setRootDialogMessage).toHaveBeenCalled();
});
});

View File

@@ -3,12 +3,10 @@
* derived from https://lingui.js.org/guides/testing.html
*/
import React from 'react';
import { shape, object, string, arrayOf, func } from 'prop-types';
import { shape, object, string, arrayOf } from 'prop-types';
import { mount, shallow } from 'enzyme';
import { I18nProvider } from '@lingui/react';
import { ConfigProvider } from '../src/contexts/Config';
import { _NetworkProvider } from '../src/contexts/Network';
import { RootDialogProvider } from '../src/contexts/RootDialog';
const language = 'en-US';
const intlProvider = new I18nProvider(
@@ -36,8 +34,6 @@ const defaultContexts = {
ansible_version: null,
custom_virtualenvs: [],
version: null,
custom_logo: null,
custom_login_info: null,
toJSON: () => '/config/'
},
router: {
@@ -69,30 +65,19 @@ const defaultContexts = {
},
toJSON: () => '/router/',
},
network: {
handleHttpError: () => {},
},
dialog: {}
};
function wrapContexts (node, context) {
const { config, network, dialog } = context;
const { config } = context;
class Wrap extends React.Component {
render () {
// eslint-disable-next-line react/no-this-in-sfc
const { children, ...props } = this.props;
const component = React.cloneElement(children, props);
return (
<RootDialogProvider value={dialog}>
<_NetworkProvider value={network}>
<ConfigProvider
value={config}
i18n={defaultContexts.linguiPublisher.i18n}
>
{component}
</ConfigProvider>
</_NetworkProvider>
</RootDialogProvider>
<ConfigProvider value={config}>
{component}
</ConfigProvider>
);
}
}
@@ -131,8 +116,6 @@ export function mountWithContexts (node, options = {}) {
ansible_version: string,
custom_virtualenvs: arrayOf(string),
version: string,
custom_logo: string,
custom_login_info: string,
}),
router: shape({
route: shape({
@@ -141,36 +124,31 @@ export function mountWithContexts (node, options = {}) {
}).isRequired,
history: shape({}).isRequired,
}),
network: shape({
handleHttpError: func.isRequired,
}),
dialog: shape({
title: string,
setRootDialogMessage: func,
clearRootDialogMessage: func,
}),
...options.childContextTypes
};
return mount(wrapContexts(node, context), { context, childContextTypes });
}
/**
* Wait for element to exist.
* Wait for element(s) to achieve a desired state.
*
* @param[wrapper] - A ReactWrapper instance
* @param[selector] - The selector of the element 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.
*/
export function waitForElement (wrapper, selector) {
export function waitForElement (wrapper, selector, callback = el => el.length === 1) {
const interval = 100;
return new Promise((resolve, reject) => {
let attempts = 30;
(function pollElement () {
wrapper.update();
if (wrapper.exists(selector)) {
return resolve(wrapper.find(selector));
const el = wrapper.find(selector);
if (callback(el)) {
return resolve(el);
}
if (--attempts <= 0) {
return reject(new Error(`Element not found using ${selector}`));
const message = `Expected condition for <${selector}> not met: ${callback.toString()}`;
return reject(new Error(message));
}
return setTimeout(pollElement, interval);
}());

View File

@@ -4,7 +4,6 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { mountWithContexts, waitForElement } from './enzymeHelpers';
import { Config } from '../src/contexts/Config';
import { withRootDialog } from '../src/contexts/RootDialog';
describe('mountWithContexts', () => {
describe('injected I18nProvider', () => {
@@ -109,68 +108,6 @@ describe('mountWithContexts', () => {
expect(wrapper.find('Foo')).toMatchSnapshot();
});
});
describe('injected root dialog', () => {
it('should mount and render', () => {
const Foo = ({ title, setRootDialogMessage }) => (
<div>
<span>{title}</span>
<button
type="button"
onClick={() => setRootDialogMessage({ title: 'error' })}
>
click
</button>
</div>
);
const Bar = withRootDialog(Foo);
const wrapper = mountWithContexts(<Bar />);
expect(wrapper.find('span').text()).toEqual('');
wrapper.find('button').simulate('click');
wrapper.update();
expect(wrapper.find('span').text()).toEqual('error');
});
it('should mount and render with stubbed value', () => {
const dialog = {
title: 'this be the title',
setRootDialogMessage: jest.fn(),
};
const Foo = ({ title, setRootDialogMessage }) => (
<div>
<span>{title}</span>
<button
type="button"
onClick={() => setRootDialogMessage('error')}
>
click
</button>
</div>
);
const Bar = withRootDialog(Foo);
const wrapper = mountWithContexts(<Bar />, { context: { dialog } });
expect(wrapper.find('span').text()).toEqual('this be the title');
wrapper.find('button').simulate('click');
expect(dialog.setRootDialogMessage).toHaveBeenCalledWith('error');
});
});
it('should set props on wrapped component', () => {
function TestComponent ({ text }) {
return (<div>{text}</div>);
}
const wrapper = mountWithContexts(
<TestComponent text="foo" />
);
expect(wrapper.find('div').text()).toEqual('foo');
wrapper.setProps({
text: 'bar'
});
expect(wrapper.find('div').text()).toEqual('bar');
});
});
/**
@@ -184,9 +121,7 @@ class TestAsyncComponent extends Component {
}
componentDidMount () {
setTimeout(() => {
this.setState({ displayElement: true });
}, 1000);
setTimeout(() => this.setState({ displayElement: true }), 500);
}
render () {
@@ -211,16 +146,15 @@ describe('waitForElement', () => {
});
it('eventually throws an error for elements that don\'t exist', async (done) => {
const selector = '#does-not-exist';
const wrapper = mountWithContexts(<div />);
let error;
try {
await waitForElement(wrapper, selector);
await waitForElement(wrapper, '#does-not-exist');
} catch (err) {
error = err;
} finally {
expect(error).toEqual(new Error(`Element not found using ${selector}`));
expect(error).toEqual(new Error('Expected condition for <#does-not-exist> not met: el => el.length === 1'));
done();
}
});

View File

@@ -1,169 +1,215 @@
import React from 'react';
import { mountWithContexts } from '../enzymeHelpers';
import { asyncFlush } from '../../jest.setup';
import { mountWithContexts, waitForElement } from '../enzymeHelpers';
import AWXLogin from '../../src/pages/Login';
import { RootAPI } from '../../src/api';
jest.mock('../../src/api');
describe('<Login />', () => {
let loginWrapper;
let awxLogin;
let loginPage;
let loginForm;
let usernameInput;
let passwordInput;
let submitButton;
let loginHeaderLogo;
async function findChildren (wrapper) {
const [
awxLogin,
loginPage,
loginForm,
usernameInput,
passwordInput,
submitButton,
loginHeaderLogo,
] = await Promise.all([
waitForElement(wrapper, 'AWXLogin', (el) => el.length === 1),
waitForElement(wrapper, 'LoginPage', (el) => el.length === 1),
waitForElement(wrapper, 'LoginForm', (el) => el.length === 1),
waitForElement(wrapper, 'input#pf-login-username-id', (el) => el.length === 1),
waitForElement(wrapper, 'input#pf-login-password-id', (el) => el.length === 1),
waitForElement(wrapper, 'Button[type="submit"]', (el) => el.length === 1),
waitForElement(wrapper, 'img', (el) => el.length === 1),
]);
return {
awxLogin,
loginPage,
loginForm,
usernameInput,
passwordInput,
submitButton,
loginHeaderLogo,
};
}
const mountLogin = () => {
loginWrapper = mountWithContexts(<AWXLogin />, { context: { network: {} } });
};
const findChildren = () => {
awxLogin = loginWrapper.find('AWXLogin');
loginPage = loginWrapper.find('LoginPage');
loginForm = loginWrapper.find('LoginForm');
usernameInput = loginWrapper.find('input#pf-login-username-id');
passwordInput = loginWrapper.find('input#pf-login-password-id');
submitButton = loginWrapper.find('Button[type="submit"]');
loginHeaderLogo = loginPage.find('img');
};
beforeEach(() => {
RootAPI.read.mockResolvedValue({
data: {
custom_login_info: '',
custom_logo: 'images/foo.jpg'
}
});
});
afterEach(() => {
jest.clearAllMocks();
loginWrapper.unmount();
});
test('initially renders without crashing', () => {
mountLogin();
findChildren();
expect(loginWrapper.length).toBe(1);
expect(loginPage.length).toBe(1);
expect(loginForm.length).toBe(1);
expect(usernameInput.length).toBe(1);
test('initially renders without crashing', async (done) => {
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const {
awxLogin,
usernameInput,
passwordInput,
submitButton,
} = await findChildren(loginWrapper);
expect(usernameInput.props().value).toBe('');
expect(passwordInput.length).toBe(1);
expect(passwordInput.props().value).toBe('');
expect(awxLogin.state().isInputValid).toBe(true);
expect(submitButton.length).toBe(1);
expect(awxLogin.state('validationError')).toBe(false);
expect(submitButton.props().isDisabled).toBe(false);
expect(loginHeaderLogo.length).toBe(1);
done();
});
test('custom logo renders Brand component with correct src and alt', () => {
loginWrapper = mountWithContexts(<AWXLogin logo="images/foo.jpg" alt="Foo Application" />);
findChildren();
expect(loginHeaderLogo.length).toBe(1);
expect(loginHeaderLogo.props().src).toBe('data:image/jpeg;images/foo.jpg');
expect(loginHeaderLogo.props().alt).toBe('Foo Application');
test('custom logo renders Brand component with correct src and alt', async (done) => {
const loginWrapper = mountWithContexts(
<AWXLogin alt="Foo Application" isAuthenticated={() => false} />
);
const { loginHeaderLogo } = await findChildren(loginWrapper);
const { alt, src } = loginHeaderLogo.props();
expect([alt, src]).toEqual(['Foo Application', 'data:image/jpeg;images/foo.jpg']);
done();
});
test('default logo renders Brand component with correct src and alt', () => {
mountLogin();
findChildren();
expect(loginHeaderLogo.length).toBe(1);
expect(loginHeaderLogo.props().src).toBe('brand-logo.svg');
expect(loginHeaderLogo.props().alt).toBe('AWX');
test('default logo renders Brand component with correct src and alt', async (done) => {
RootAPI.read.mockResolvedValue({ data: {} });
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const { loginHeaderLogo } = await findChildren(loginWrapper);
const { alt, src } = loginHeaderLogo.props();
expect([alt, src]).toEqual(['AWX', 'brand-logo.svg']);
done();
});
test('state maps to un/pw input value props', () => {
mountLogin();
findChildren();
awxLogin.setState({ username: 'un', password: 'pw' });
expect(awxLogin.state().username).toBe('un');
expect(awxLogin.state().password).toBe('pw');
findChildren();
expect(usernameInput.props().value).toBe('un');
expect(passwordInput.props().value).toBe('pw');
test('default logo renders on data initialization error', async (done) => {
RootAPI.read.mockRejectedValueOnce({ response: { status: 500 } });
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const { loginHeaderLogo } = await findChildren(loginWrapper);
const { alt, src } = loginHeaderLogo.props();
expect([alt, src]).toEqual(['AWX', 'brand-logo.svg']);
done();
});
test('updating un/pw clears out error', () => {
mountLogin();
findChildren();
awxLogin.setState({ isInputValid: false });
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
usernameInput.instance().value = 'uname';
usernameInput.simulate('change');
expect(awxLogin.state().username).toBe('uname');
expect(awxLogin.state().isInputValid).toBe(true);
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
awxLogin.setState({ isInputValid: false });
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(1);
passwordInput.instance().value = 'pword';
passwordInput.simulate('change');
expect(awxLogin.state().password).toBe('pword');
expect(awxLogin.state().isInputValid).toBe(true);
expect(loginWrapper.find('.pf-c-form__helper-text.pf-m-error').length).toBe(0);
test('state maps to un/pw input value props', async (done) => {
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const { usernameInput, passwordInput } = await findChildren(loginWrapper);
usernameInput.props().onChange({ currentTarget: { value: 'un' } });
passwordInput.props().onChange({ currentTarget: { value: 'pw' } });
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'un');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'pw');
done();
});
test('login API not called when loading', () => {
mountLogin();
findChildren();
expect(awxLogin.state().isLoading).toBe(false);
awxLogin.setState({ isLoading: true });
test('handles input validation errors and clears on input value change', async (done) => {
const formError = '.pf-c-form__helper-text.pf-m-error';
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const {
usernameInput,
passwordInput,
submitButton
} = await findChildren(loginWrapper);
RootAPI.login.mockRejectedValueOnce({ response: { status: 401 } });
usernameInput.props().onChange({ currentTarget: { value: 'invalid' } });
passwordInput.props().onChange({ currentTarget: { value: 'invalid' } });
submitButton.simulate('click');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'invalid');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'invalid');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('validationError') === true);
await waitForElement(loginWrapper, formError, (el) => el.length === 1);
usernameInput.props().onChange({ currentTarget: { value: 'dsarif' } });
passwordInput.props().onChange({ currentTarget: { value: 'freneticpny' } });
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'dsarif');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'freneticpny');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('validationError') === false);
await waitForElement(loginWrapper, formError, (el) => el.length === 0);
done();
});
test('handles other errors and clears on resubmit', async (done) => {
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const {
usernameInput,
passwordInput,
submitButton
} = await findChildren(loginWrapper);
RootAPI.login.mockRejectedValueOnce({ response: { status: 500 } });
submitButton.simulate('click');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('authenticationError') === true);
usernameInput.props().onChange({ currentTarget: { value: 'sgrimes' } });
passwordInput.props().onChange({ currentTarget: { value: 'ovid' } });
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('username') === 'sgrimes');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('password') === 'ovid');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('authenticationError') === true);
submitButton.simulate('click');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('authenticationError') === false);
done();
});
test('no login requests are made when already authenticating', async (done) => {
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const { awxLogin, submitButton } = await findChildren(loginWrapper);
awxLogin.setState({ isAuthenticating: true });
submitButton.simulate('click');
submitButton.simulate('click');
expect(RootAPI.login).toHaveBeenCalledTimes(0);
});
test('submit calls login API successfully', async () => {
RootAPI.login = jest.fn().mockImplementation(() => Promise.resolve({}));
mountLogin();
findChildren();
expect(awxLogin.state().isLoading).toBe(false);
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
awxLogin.setState({ isAuthenticating: false });
submitButton.simulate('click');
submitButton.simulate('click');
expect(RootAPI.login).toHaveBeenCalledTimes(1);
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(awxLogin.state().isLoading).toBe(true);
await asyncFlush();
expect(awxLogin.state().isLoading).toBe(false);
done();
});
test('submit calls login API and handles 401 error', async () => {
RootAPI.login = jest.fn().mockImplementation(() => {
const err = new Error('401 error');
err.response = { status: 401, message: 'problem' };
return Promise.reject(err);
});
mountLogin();
findChildren();
expect(awxLogin.state().isLoading).toBe(false);
expect(awxLogin.state().isInputValid).toBe(true);
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
test('submit calls api.login successfully', async (done) => {
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => false} />
);
const {
usernameInput,
passwordInput,
submitButton,
} = await findChildren(loginWrapper);
usernameInput.props().onChange({ currentTarget: { value: 'gthorpe' } });
passwordInput.props().onChange({ currentTarget: { value: 'hydro' } });
submitButton.simulate('click');
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('isAuthenticating') === true);
await waitForElement(loginWrapper, 'AWXLogin', (el) => el.state('isAuthenticating') === false);
expect(RootAPI.login).toHaveBeenCalledTimes(1);
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(awxLogin.state().isLoading).toBe(true);
await asyncFlush();
expect(awxLogin.state().isInputValid).toBe(false);
expect(awxLogin.state().isLoading).toBe(false);
expect(RootAPI.login).toHaveBeenCalledWith('gthorpe', 'hydro');
done();
});
test('submit calls login API and handles non-401 error', async () => {
RootAPI.login = jest.fn().mockImplementation(() => {
const err = new Error('500 error');
err.response = { status: 500, message: 'problem' };
return Promise.reject(err);
});
mountLogin();
findChildren();
expect(awxLogin.state().isLoading).toBe(false);
awxLogin.setState({ username: 'unamee', password: 'pwordd' });
submitButton.simulate('click');
expect(RootAPI.login).toHaveBeenCalledTimes(1);
expect(RootAPI.login).toHaveBeenCalledWith('unamee', 'pwordd');
expect(awxLogin.state().isLoading).toBe(true);
await asyncFlush();
expect(awxLogin.state().isLoading).toBe(false);
});
test('render Redirect to / when already authenticated', () => {
mountLogin();
findChildren();
awxLogin.setState({ isAuthenticated: true });
const redirectElem = loginWrapper.find('Redirect');
expect(redirectElem.length).toBe(1);
expect(redirectElem.props().to).toBe('/');
test('render Redirect to / when already authenticated', async (done) => {
const loginWrapper = mountWithContexts(
<AWXLogin isAuthenticated={() => true} />
);
await waitForElement(loginWrapper, 'Redirect', (el) => el.length === 1);
await waitForElement(loginWrapper, 'Redirect', (el) => el.props().to === '/');
done();
});
});

View File

@@ -2,6 +2,8 @@ import React from 'react';
import { mountWithContexts } from '../../enzymeHelpers';
import Organizations from '../../../src/pages/Organizations/Organizations';
jest.mock('../../../src/api');
describe('<Organizations />', () => {
test('initially renders succesfully', () => {
mountWithContexts(

View File

@@ -1,63 +1,232 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../../enzymeHelpers';
import { sleep } from '../../../../testUtils';
import { mountWithContexts, waitForElement } from '../../../../enzymeHelpers';
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';
import { OrganizationsAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
describe.only('<Organization />', () => {
const me = {
is_super_user: true,
is_system_auditor: false
};
const mockMe = {
is_super_user: true,
is_system_auditor: false
};
const mockNoResults = {
count: 0,
next: null,
previous: null,
data: { results: [] }
};
const mockDetails = {
data: {
id: 1,
type: 'organization',
url: '/api/v2/organizations/1/',
related: {
notification_templates: '/api/v2/organizations/1/notification_templates/',
notification_templates_any: '/api/v2/organizations/1/notification_templates_any/',
notification_templates_success: '/api/v2/organizations/1/notification_templates_success/',
notification_templates_error: '/api/v2/organizations/1/notification_templates_error/',
object_roles: '/api/v2/organizations/1/object_roles/',
access_list: '/api/v2/organizations/1/access_list/',
instance_groups: '/api/v2/organizations/1/instance_groups/'
},
summary_fields: {
created_by: {
id: 1,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
modified_by: {
id: 1,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
object_roles: {
admin_role: {
description: 'Can manage all aspects of the organization',
name: 'Admin',
id: 42
},
notification_admin_role: {
description: 'Can manage all notifications of the organization',
name: 'Notification Admin',
id: 1683
},
auditor_role: {
description: 'Can view all aspects of the organization',
name: 'Auditor',
id: 41
},
},
user_capabilities: {
edit: true,
delete: true
},
related_field_counts: {
users: 51,
admins: 19,
inventories: 23,
teams: 12,
projects: 33,
job_templates: 30
}
},
created: '2015-07-07T17:21:26.429745Z',
modified: '2017-09-05T19:23:15.418808Z',
name: 'Sarif Industries',
description: '',
max_hosts: 0,
custom_virtualenv: null
}
};
const adminOrganization = {
id: 1,
type: 'organization',
url: '/api/v2/organizations/1/',
related: {
instance_groups: '/api/v2/organizations/1/instance_groups/',
object_roles: '/api/v2/organizations/1/object_roles/',
access_list: '/api/v2/organizations/1/access_list/',
},
summary_fields: {
created_by: {
id: 1,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
modified_by: {
id: 1,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
},
created: '2015-07-07T17:21:26.429745Z',
modified: '2017-09-05T19:23:15.418808Z',
name: 'Sarif Industries',
description: '',
max_hosts: 0,
custom_virtualenv: null
};
const auditorOrganization = {
id: 2,
type: 'organization',
url: '/api/v2/organizations/2/',
related: {
instance_groups: '/api/v2/organizations/2/instance_groups/',
object_roles: '/api/v2/organizations/2/object_roles/',
access_list: '/api/v2/organizations/2/access_list/',
},
summary_fields: {
created_by: {
id: 2,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
modified_by: {
id: 2,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
},
created: '2015-07-07T17:21:26.429745Z',
modified: '2017-09-05T19:23:15.418808Z',
name: 'Autobots',
description: '',
max_hosts: 0,
custom_virtualenv: null
};
const notificationAdminOrganization = {
id: 3,
type: 'organization',
url: '/api/v2/organizations/3/',
related: {
instance_groups: '/api/v2/organizations/3/instance_groups/',
object_roles: '/api/v2/organizations/3/object_roles/',
access_list: '/api/v2/organizations/3/access_list/',
},
summary_fields: {
created_by: {
id: 1,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
modified_by: {
id: 1,
username: 'admin',
first_name: 'Super',
last_name: 'User'
},
},
created: '2015-07-07T17:21:26.429745Z',
modified: '2017-09-05T19:23:15.418808Z',
name: 'Decepticons',
description: '',
max_hosts: 0,
custom_virtualenv: null
};
const allOrganizations = [
adminOrganization,
auditorOrganization,
notificationAdminOrganization
];
async function getOrganizations (params) {
let results = allOrganizations;
if (params && params.role_level) {
if (params.role_level === 'admin_role') {
results = [adminOrganization];
}
if (params.role_level === 'auditor_role') {
results = [auditorOrganization];
}
if (params.role_level === 'notification_admin_role') {
results = [notificationAdminOrganization];
}
}
return {
count: results.length,
next: null,
previous: null,
data: { results }
};
}
describe.only('<Organization />', () => {
test('initially renders succesfully', () => {
mountWithContexts(<Organization me={me} />);
OrganizationsAPI.readDetail.mockResolvedValue(mockDetails);
OrganizationsAPI.read.mockImplementation(getOrganizations);
mountWithContexts(<Organization setBreadcrumb={() => {}} me={mockMe} />);
});
test('notifications tab shown/hidden based on permissions', async () => {
OrganizationsAPI.readDetail.mockResolvedValue({
data: {
id: 1,
name: 'foo'
}
});
OrganizationsAPI.read.mockResolvedValue({
data: {
results: []
}
});
const history = createMemoryHistory({
initialEntries: ['/organizations/1/details'],
});
const match = { path: '/organizations/:id', url: '/organizations/1' };
const wrapper = mountWithContexts(
<Organization
me={me}
setBreadcrumb={() => {}}
/>,
{
context: {
router: {
history,
route: {
location: history.location,
match
}
}
}
}
);
await sleep(0);
wrapper.update();
expect(wrapper.find('.pf-c-tabs__item').length).toBe(3);
expect(wrapper.find('.pf-c-tabs__button[children="Notifications"]').length).toBe(0);
wrapper.find('Organization').setState({
isNotifAdmin: true
});
expect(wrapper.find('.pf-c-tabs__item').length).toBe(4);
expect(wrapper.find('button.pf-c-tabs__button[children="Notifications"]').length).toBe(1);
test('notifications tab shown for admins', async (done) => {
OrganizationsAPI.readDetail.mockResolvedValue(mockDetails);
OrganizationsAPI.read.mockImplementation(getOrganizations);
const wrapper = mountWithContexts(<Organization setBreadcrumb={() => {}} me={mockMe} />);
const tabs = await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 4);
expect(tabs.last().text()).toEqual('Notifications');
done();
});
test('notifications tab hidden with reduced permissions', async (done) => {
OrganizationsAPI.readDetail.mockResolvedValue(mockDetails);
OrganizationsAPI.read.mockResolvedValue(mockNoResults);
const wrapper = mountWithContexts(<Organization setBreadcrumb={() => {}} me={mockMe} />);
const tabs = await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 3);
tabs.forEach(tab => expect(tab.text()).not.toEqual('Notifications'));
done();
});
});

View File

@@ -1,14 +1,12 @@
import React from 'react';
import { mountWithContexts } from '../../../../enzymeHelpers';
import { mountWithContexts, waitForElement } from '../../../../enzymeHelpers';
import OrganizationAccess from '../../../../../src/pages/Organizations/screens/Organization/OrganizationAccess';
import { sleep } from '../../../../testUtils';
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
describe('<OrganizationAccess />', () => {
const network = {};
const organization = {
id: 1,
name: 'Default',
@@ -64,7 +62,9 @@ describe('<OrganizationAccess />', () => {
};
beforeEach(() => {
OrganizationsAPI.readAccessList.mockReturnValue({ data });
OrganizationsAPI.readAccessList.mockResolvedValue({ data });
TeamsAPI.disassociateRole.mockResolvedValue({});
UsersAPI.disassociateRole.mockResolvedValue({});
});
afterEach(() => {
@@ -72,31 +72,21 @@ describe('<OrganizationAccess />', () => {
});
test('initially renders succesfully', () => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />,
{ context: { network } }
);
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
expect(wrapper.find('OrganizationAccess')).toMatchSnapshot();
});
test('should fetch and display access records on mount', async () => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />,
{ context: { network } }
);
await sleep(0);
wrapper.update();
expect(OrganizationsAPI.readAccessList).toHaveBeenCalled();
expect(wrapper.find('OrganizationAccess').state('isInitialized')).toBe(true);
test('should fetch and display access records on mount', async (done) => {
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
await waitForElement(wrapper, 'OrganizationAccessItem', el => el.length === 2);
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(data.results);
expect(wrapper.find('OrganizationAccessItem')).toHaveLength(2);
expect(wrapper.find('OrganizationAccess').state('contentLoading')).toBe(false);
expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(false);
done();
});
test('should open confirmation dialog when deleting role', async () => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />,
{ context: { network } }
);
test('should open confirmation dialog when deleting role', async (done) => {
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
await sleep(0);
wrapper.update();
@@ -105,18 +95,16 @@ describe('<OrganizationAccess />', () => {
wrapper.update();
const component = wrapper.find('OrganizationAccess');
expect(component.state('roleToDelete'))
expect(component.state('deletionRole'))
.toEqual(data.results[0].summary_fields.direct_access[0].role);
expect(component.state('roleToDeleteAccessRecord'))
expect(component.state('deletionRecord'))
.toEqual(data.results[0]);
expect(component.find('DeleteRoleConfirmationModal')).toHaveLength(1);
done();
});
it('should close dialog when cancel button clicked', async () => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />,
{ context: { network } }
);
it('should close dialog when cancel button clicked', async (done) => {
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
await sleep(0);
wrapper.update();
const button = wrapper.find('ChipButton').at(0);
@@ -125,55 +113,50 @@ describe('<OrganizationAccess />', () => {
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
const component = wrapper.find('OrganizationAccess');
expect(component.state('roleToDelete')).toBeNull();
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
expect(component.state('deletionRole')).toBeNull();
expect(component.state('deletionRecord')).toBeNull();
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
done();
});
it('should delete user role', async () => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />,
{ context: { network } }
);
it('should delete user role', async (done) => {
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
const button = await waitForElement(wrapper, 'ChipButton', el => el.length === 2);
button.at(0).prop('onClick')();
const confirmation = await waitForElement(wrapper, 'DeleteRoleConfirmationModal');
confirmation.prop('onConfirm')();
await waitForElement(wrapper, 'DeleteRoleConfirmationModal', el => el.length === 0);
await sleep(0);
wrapper.update();
const button = wrapper.find('ChipButton').at(0);
button.prop('onClick')();
wrapper.update();
wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
await sleep(0);
wrapper.update();
const component = wrapper.find('OrganizationAccess');
expect(component.state('roleToDelete')).toBeNull();
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
expect(component.state('deletionRole')).toBeNull();
expect(component.state('deletionRecord')).toBeNull();
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
expect(UsersAPI.disassociateRole).toHaveBeenCalledWith(1, 1);
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
done();
});
it('should delete team role', async () => {
const wrapper = mountWithContexts(
<OrganizationAccess organization={organization} />,
{ context: { network } }
);
it('should delete team role', async (done) => {
const wrapper = mountWithContexts(<OrganizationAccess organization={organization} />);
const button = await waitForElement(wrapper, 'ChipButton', el => el.length === 2);
button.at(1).prop('onClick')();
const confirmation = await waitForElement(wrapper, 'DeleteRoleConfirmationModal');
confirmation.prop('onConfirm')();
await waitForElement(wrapper, 'DeleteRoleConfirmationModal', el => el.length === 0);
await sleep(0);
wrapper.update();
const button = wrapper.find('ChipButton').at(1);
button.prop('onClick')();
wrapper.update();
wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
await sleep(0);
wrapper.update();
const component = wrapper.find('OrganizationAccess');
expect(component.state('roleToDelete')).toBeNull();
expect(component.state('roleToDeleteAccessRecord')).toBeNull();
expect(component.state('deletionRole')).toBeNull();
expect(component.state('deletionRecord')).toBeNull();
expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3);
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
done();
});
});

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { mountWithContexts } from '../../../../enzymeHelpers';
import { mountWithContexts, waitForElement } from '../../../../enzymeHelpers';
import OrganizationDetail from '../../../../../src/pages/Organizations/screens/Organization/OrganizationDetail';
import { OrganizationsAPI } from '../../../../../src/api';
jest.mock('../../../../../src/api');
describe('<OrganizationDetail />', () => {
const mockDetails = {
const mockOrganization = {
name: 'Foo',
description: 'Bar',
custom_virtualenv: 'Fizz',
@@ -19,107 +19,75 @@ describe('<OrganizationDetail />', () => {
}
}
};
const mockInstanceGroups = {
data: {
results: [
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
]
}
};
beforeEach(() => {
OrganizationsAPI.readInstanceGroups.mockResolvedValue(mockInstanceGroups);
});
afterEach(() => {
jest.clearAllMocks();
});
test('initially renders succesfully', () => {
mountWithContexts(
<OrganizationDetail
organization={mockDetails}
/>
);
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
});
test('should request instance groups from api', () => {
mountWithContexts(
<OrganizationDetail
organization={mockDetails}
/>, { context: {
network: { handleHttpError: () => {} }
} }
).find('OrganizationDetail');
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
expect(OrganizationsAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
});
test('should handle setting instance groups to state', async () => {
const mockInstanceGroups = [
{ name: 'One', id: 1 },
{ name: 'Two', id: 2 }
];
OrganizationsAPI.readInstanceGroups.mockResolvedValue({
data: { results: mockInstanceGroups }
});
test('should handle setting instance groups to state', async (done) => {
const wrapper = mountWithContexts(
<OrganizationDetail
organization={mockDetails}
/>, { context: {
network: { handleHttpError: () => {} }
} }
).find('OrganizationDetail');
await OrganizationsAPI.readInstanceGroups();
expect(wrapper.state().instanceGroups).toEqual(mockInstanceGroups);
});
test('should render Details', async () => {
const wrapper = mountWithContexts(
<OrganizationDetail
organization={mockDetails}
/>
<OrganizationDetail organization={mockOrganization} />
);
const detailWrapper = wrapper.find('Detail');
expect(detailWrapper.length).toBe(6);
const nameDetail = detailWrapper.findWhere(node => node.props().label === 'Name');
const descriptionDetail = detailWrapper.findWhere(node => node.props().label === 'Description');
const custom_virtualenvDetail = detailWrapper.findWhere(node => node.props().label === 'Ansible Environment');
const max_hostsDetail = detailWrapper.findWhere(node => node.props().label === 'Max Hosts');
const createdDetail = detailWrapper.findWhere(node => node.props().label === 'Created');
const modifiedDetail = detailWrapper.findWhere(node => node.props().label === 'Last Modified');
expect(nameDetail.find('dt').text()).toBe('Name');
expect(nameDetail.find('dd').text()).toBe('Foo');
expect(descriptionDetail.find('dt').text()).toBe('Description');
expect(descriptionDetail.find('dd').text()).toBe('Bar');
expect(custom_virtualenvDetail.find('dt').text()).toBe('Ansible Environment');
expect(custom_virtualenvDetail.find('dd').text()).toBe('Fizz');
expect(createdDetail.find('dt').text()).toBe('Created');
expect(createdDetail.find('dd').text()).toBe('Bat');
expect(modifiedDetail.find('dt').text()).toBe('Last Modified');
expect(modifiedDetail.find('dd').text()).toBe('Boo');
expect(max_hostsDetail.find('dt').text()).toBe('Max Hosts');
expect(max_hostsDetail.find('dd').text()).toBe('0');
const component = await waitForElement(wrapper, 'OrganizationDetail');
expect(component.state().instanceGroups).toEqual(mockInstanceGroups.data.results);
done();
});
test('should show edit button for users with edit permission', () => {
const wrapper = mountWithContexts(
<OrganizationDetail
organization={mockDetails}
/>
).find('OrganizationDetail');
const editButton = wrapper.find('Button');
expect((editButton).prop('to')).toBe('/organizations/undefined/edit');
test('should render Details', async (done) => {
const wrapper = mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
const testParams = [
{ label: 'Name', value: 'Foo' },
{ label: 'Description', value: 'Bar' },
{ label: 'Ansible Environment', value: 'Fizz' },
{ label: 'Created', value: 'Bat' },
{ label: 'Last Modified', value: 'Boo' },
{ label: 'Max Hosts', value: '0' },
];
// eslint-disable-next-line no-restricted-syntax
for (const { label, value } of testParams) {
// eslint-disable-next-line no-await-in-loop
const detail = await waitForElement(wrapper, `Detail[label="${label}"]`);
expect(detail.find('dt').text()).toBe(label);
expect(detail.find('dd').text()).toBe(value);
}
done();
});
test('should hide edit button for users without edit permission', () => {
const readOnlyOrg = { ...mockDetails };
test('should show edit button for users with edit permission', async (done) => {
const wrapper = mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
const editButton = await waitForElement(wrapper, 'OrganizationDetail Button');
expect(editButton.text()).toEqual('Edit');
expect(editButton.prop('to')).toBe('/organizations/undefined/edit');
done();
});
test('should hide edit button for users without edit permission', async (done) => {
const readOnlyOrg = { ...mockOrganization };
readOnlyOrg.summary_fields.user_capabilities.edit = false;
const wrapper = mountWithContexts(
<OrganizationDetail
organization={readOnlyOrg}
/>
).find('OrganizationDetail');
const editLink = wrapper
.findWhere(node => node.props().to === '/organizations/undefined/edit');
expect(editLink.length).toBe(0);
const wrapper = mountWithContexts(<OrganizationDetail organization={readOnlyOrg} />);
await waitForElement(wrapper, 'OrganizationDetail');
expect(wrapper.find('OrganizationDetail Button').length).toBe(0);
done();
});
});

View File

@@ -37,7 +37,6 @@ describe('<OrganizationEdit />', () => {
organization={mockData}
/>, { context: { network: {
api,
handleHttpError: () => {}
} } }
);
@@ -57,7 +56,6 @@ describe('<OrganizationEdit />', () => {
organization={mockData}
/>, { context: { network: {
api,
handleHttpError: () => {}
} } }
);
@@ -84,7 +82,6 @@ describe('<OrganizationEdit />', () => {
/>, { context: {
network: {
api: { api },
handleHttpError: () => {}
},
router: { history }
} }

View File

@@ -8,7 +8,6 @@ jest.mock('../../../../../src/api');
describe('<OrganizationNotifications />', () => {
let data;
const network = {};
beforeEach(() => {
data = {
@@ -40,8 +39,7 @@ describe('<OrganizationNotifications />', () => {
test('initially renders succesfully', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />,
{ context: { network } }
<OrganizationNotifications id={1} canToggleNotifications />
);
await sleep(0);
wrapper.update();
@@ -50,10 +48,7 @@ describe('<OrganizationNotifications />', () => {
test('should render list fetched of items', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />,
{
context: { network }
}
<OrganizationNotifications id={1} canToggleNotifications />
);
await sleep(0);
wrapper.update();
@@ -71,10 +66,7 @@ describe('<OrganizationNotifications />', () => {
test('should enable success notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />,
{
context: { network }
}
<OrganizationNotifications id={1} canToggleNotifications />
);
await sleep(0);
wrapper.update();
@@ -84,7 +76,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([1]);
const items = wrapper.find('NotificationListItem');
items.at(1).find('Switch').at(0).prop('onChange')();
expect(OrganizationsAPI.associateNotificationTemplatesSuccess).toHaveBeenCalled();
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 2, 'success', true);
await sleep(0);
wrapper.update();
expect(
@@ -94,10 +86,7 @@ describe('<OrganizationNotifications />', () => {
test('should enable error notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />,
{
context: { network }
}
<OrganizationNotifications id={1} canToggleNotifications />
);
await sleep(0);
wrapper.update();
@@ -107,7 +96,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([2]);
const items = wrapper.find('NotificationListItem');
items.at(0).find('Switch').at(1).prop('onChange')();
expect(OrganizationsAPI.associateNotificationTemplatesError).toHaveBeenCalled();
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 1, 'error', true);
await sleep(0);
wrapper.update();
expect(
@@ -117,10 +106,7 @@ describe('<OrganizationNotifications />', () => {
test('should disable success notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />,
{
context: { network }
}
<OrganizationNotifications id={1} canToggleNotifications />
);
await sleep(0);
wrapper.update();
@@ -130,7 +116,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([1]);
const items = wrapper.find('NotificationListItem');
items.at(0).find('Switch').at(0).prop('onChange')();
expect(OrganizationsAPI.disassociateNotificationTemplatesSuccess).toHaveBeenCalled();
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 1, 'success', false);
await sleep(0);
wrapper.update();
expect(
@@ -140,10 +126,7 @@ describe('<OrganizationNotifications />', () => {
test('should disable error notification', async () => {
const wrapper = mountWithContexts(
<OrganizationNotifications id={1} canToggleNotifications />,
{
context: { network }
}
<OrganizationNotifications id={1} canToggleNotifications />
);
await sleep(0);
wrapper.update();
@@ -153,7 +136,7 @@ describe('<OrganizationNotifications />', () => {
).toEqual([2]);
const items = wrapper.find('NotificationListItem');
items.at(1).find('Switch').at(1).prop('onChange')();
expect(OrganizationsAPI.disassociateNotificationTemplatesError).toHaveBeenCalled();
expect(OrganizationsAPI.updateNotificationTemplateAssociation).toHaveBeenCalledWith(1, 2, 'error', false);
await sleep(0);
wrapper.update();
expect(

View File

@@ -20,22 +20,21 @@ const listData = {
}
};
beforeEach(() => {
OrganizationsAPI.readTeams.mockResolvedValue(listData);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('<OrganizationTeams />', () => {
beforeEach(() => {
OrganizationsAPI.readTeams.mockResolvedValue(listData);
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders succesfully', () => {
shallow(
<_OrganizationTeams
id={1}
searchString=""
location={{ search: '', pathname: '/organizations/1/teams' }}
handleHttpError={() => {}}
/>
);
});
@@ -45,9 +44,7 @@ describe('<OrganizationTeams />', () => {
<OrganizationTeams
id={1}
searchString=""
/>, { context: {
network: {} }
}
/>
).find('OrganizationTeams');
expect(OrganizationsAPI.readTeams).toHaveBeenCalledWith(1, {
page: 1,
@@ -61,9 +58,7 @@ describe('<OrganizationTeams />', () => {
<OrganizationTeams
id={1}
searchString=""
/>, { context: {
network: { handleHttpError: () => {} } }
}
/>
);
await sleep(0);

View File

@@ -2,7 +2,6 @@
exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
<OrganizationAccess
handleHttpError={[Function]}
history={"/history/"}
i18n={"/i18n/"}
location={
@@ -34,8 +33,228 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
}
}
>
<div>
Loading...
</div>
<WithI18n
contentError={false}
contentLoading={true}
itemCount={0}
itemName="role"
items={Array []}
qsConfig={
Object {
"defaultParams": Object {
"order_by": "first_name",
"page": 1,
"page_size": 5,
},
"integerFields": Array [
"page",
"page_size",
],
"namespace": "access",
}
}
renderItem={[Function]}
renderToolbar={[Function]}
toolbarColumns={
Array [
Object {
"isSortable": true,
"key": "first_name",
"name": "Name",
},
Object {
"isSortable": true,
"key": "username",
"name": "Username",
},
Object {
"isSortable": true,
"key": "last_name",
"name": "Last Name",
},
]
}
>
<I18n
update={true}
withHash={true}
>
<withRouter(PaginatedDataList)
contentError={false}
contentLoading={true}
i18n={"/i18n/"}
itemCount={0}
itemName="role"
items={Array []}
qsConfig={
Object {
"defaultParams": Object {
"order_by": "first_name",
"page": 1,
"page_size": 5,
},
"integerFields": Array [
"page",
"page_size",
],
"namespace": "access",
}
}
renderItem={[Function]}
renderToolbar={[Function]}
toolbarColumns={
Array [
Object {
"isSortable": true,
"key": "first_name",
"name": "Name",
},
Object {
"isSortable": true,
"key": "username",
"name": "Username",
},
Object {
"isSortable": true,
"key": "last_name",
"name": "Last Name",
},
]
}
>
<Route>
<PaginatedDataList
contentError={false}
contentLoading={true}
history={"/history/"}
i18n={"/i18n/"}
itemCount={0}
itemName="role"
itemNamePlural=""
items={Array []}
location={
Object {
"hash": "",
"pathname": "",
"search": "",
"state": "",
}
}
match={
Object {
"isExact": false,
"params": Object {},
"path": "",
"url": "",
}
}
qsConfig={
Object {
"defaultParams": Object {
"order_by": "first_name",
"page": 1,
"page_size": 5,
},
"integerFields": Array [
"page",
"page_size",
],
"namespace": "access",
}
}
renderItem={[Function]}
renderToolbar={[Function]}
showPageSizeOptions={true}
toolbarColumns={
Array [
Object {
"isSortable": true,
"key": "first_name",
"name": "Name",
},
Object {
"isSortable": true,
"key": "username",
"name": "Username",
},
Object {
"isSortable": true,
"key": "last_name",
"name": "Last Name",
},
]
}
>
<WithI18n>
<I18n
update={true}
withHash={true}
>
<ContentLoading
i18n={"/i18n/"}
>
<EmptyState
className=""
variant="large"
>
<div
className="pf-c-empty-state pf-m-lg"
>
<EmptyStateBody
className=""
>
<p
className="pf-c-empty-state__body"
>
Loading...
</p>
</EmptyStateBody>
</div>
</EmptyState>
</ContentLoading>
</I18n>
</WithI18n>
</PaginatedDataList>
</Route>
</withRouter(PaginatedDataList)>
</I18n>
</WithI18n>
<_default
isOpen={false}
onClose={[Function]}
title="Error!"
variant="danger"
>
<Modal
actions={Array []}
ariaDescribedById=""
className="awx-c-modal at-c-alertModal at-c-alertModal--danger"
hideTitle={false}
isLarge={false}
isOpen={false}
isSmall={false}
onClose={[Function]}
title="Error!"
width={null}
>
<Portal
containerInfo={<div />}
>
<ModalContent
actions={Array []}
ariaDescribedById=""
className="awx-c-modal at-c-alertModal at-c-alertModal--danger"
hideTitle={false}
id="pf-modal-0"
isLarge={false}
isOpen={false}
isSmall={false}
onClose={[Function]}
title="Error!"
width={null}
/>
</Portal>
</Modal>
</_default>
</OrganizationAccess>
`;

View File

@@ -1,27 +1,15 @@
import React from 'react';
import { mountWithContexts } from '../../../enzymeHelpers';
import { mountWithContexts, waitForElement } from '../../../enzymeHelpers';
import OrganizationAdd from '../../../../src/pages/Organizations/screens/OrganizationAdd';
import { OrganizationsAPI } from '../../../../src/api';
jest.mock('../../../../src/api');
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
describe('<OrganizationAdd />', () => {
let networkProviderValue;
beforeEach(() => {
networkProviderValue = {
handleHttpError: () => {}
};
});
test('handleSubmit should post to api', () => {
const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { network: networkProviderValue }
});
const wrapper = mountWithContexts(<OrganizationAdd />);
const updatedOrgData = {
name: 'new name',
description: 'new description',
@@ -35,9 +23,10 @@ describe('<OrganizationAdd />', () => {
const history = {
push: jest.fn(),
};
const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { router: { history } }
});
const wrapper = mountWithContexts(
<OrganizationAdd />,
{ context: { router: { history } } }
);
expect(history.push).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(history.push).toHaveBeenCalledWith('/organizations');
@@ -47,15 +36,16 @@ describe('<OrganizationAdd />', () => {
const history = {
push: jest.fn(),
};
const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { router: { history } }
});
const wrapper = mountWithContexts(
<OrganizationAdd />,
{ context: { router: { history } } }
);
expect(history.push).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Close"]').prop('onClick')();
expect(history.push).toHaveBeenCalledWith('/organizations');
});
test('successful form submission should trigger redirect', async () => {
test('successful form submission should trigger redirect', async (done) => {
const history = {
push: jest.fn(),
};
@@ -64,7 +54,7 @@ describe('<OrganizationAdd />', () => {
description: 'new description',
custom_virtualenv: 'Buzz',
};
OrganizationsAPI.create.mockReturnValueOnce({
OrganizationsAPI.create.mockResolvedValueOnce({
data: {
id: 5,
related: {
@@ -73,24 +63,23 @@ describe('<OrganizationAdd />', () => {
...orgData,
}
});
const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { router: { history }, network: networkProviderValue }
});
wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [], []);
await sleep(0);
const wrapper = mountWithContexts(
<OrganizationAdd />,
{ context: { router: { history } } }
);
await waitForElement(wrapper, 'button[aria-label="Save"]');
await wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
expect(history.push).toHaveBeenCalledWith('/organizations/5');
done();
});
test('handleSubmit should post instance groups', async () => {
const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { network: networkProviderValue }
});
test('handleSubmit should post instance groups', async (done) => {
const orgData = {
name: 'new name',
description: 'new description',
custom_virtualenv: 'Buzz',
};
OrganizationsAPI.create.mockReturnValueOnce({
OrganizationsAPI.create.mockResolvedValueOnce({
data: {
id: 5,
related: {
@@ -99,19 +88,22 @@ describe('<OrganizationAdd />', () => {
...orgData,
}
});
wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
await sleep(0);
const wrapper = mountWithContexts(<OrganizationAdd />);
await waitForElement(wrapper, 'button[aria-label="Save"]');
await wrapper.find('OrganizationForm').prop('handleSubmit')(orgData, [3], []);
expect(OrganizationsAPI.associateInstanceGroup)
.toHaveBeenCalledWith(5, 3);
done();
});
test('AnsibleSelect component renders if there are virtual environments', () => {
const config = {
custom_virtualenvs: ['foo', 'bar'],
};
const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { network: networkProviderValue, config }
}).find('AnsibleSelect');
const wrapper = mountWithContexts(
<OrganizationAdd />,
{ context: { config } }
).find('AnsibleSelect');
expect(wrapper.find('FormSelect')).toHaveLength(1);
expect(wrapper.find('FormSelectOption')).toHaveLength(2);
});
@@ -120,9 +112,10 @@ describe('<OrganizationAdd />', () => {
const config = {
custom_virtualenvs: [],
};
const wrapper = mountWithContexts(<OrganizationAdd />, {
context: { network: networkProviderValue, config }
}).find('AnsibleSelect');
const wrapper = mountWithContexts(
<OrganizationAdd />,
{ context: { config } }
).find('AnsibleSelect');
expect(wrapper.find('FormSelect')).toHaveLength(0);
});
});

View File

@@ -59,25 +59,13 @@ const mockAPIOrgsList = {
describe('<OrganizationsList />', () => {
let wrapper;
let api;
beforeEach(() => {
api = {
getOrganizations: () => {},
destroyOrganization: jest.fn(),
};
});
test('initially renders succesfully', () => {
mountWithContexts(
<OrganizationsList />
);
mountWithContexts(<OrganizationsList />);
});
test('Puts 1 selected Org in state when handleSelect is called.', () => {
wrapper = mountWithContexts(
<OrganizationsList />
).find('OrganizationsList');
wrapper = mountWithContexts(<OrganizationsList />).find('OrganizationsList');
wrapper.setState({
organizations: mockAPIOrgsList.data.results,
@@ -91,9 +79,7 @@ describe('<OrganizationsList />', () => {
});
test('Puts all Orgs in state when handleSelectAll is called.', () => {
wrapper = mountWithContexts(
<OrganizationsList />
);
wrapper = mountWithContexts(<OrganizationsList />);
const list = wrapper.find('OrganizationsList');
list.setState({
organizations: mockAPIOrgsList.data.results,
@@ -108,16 +94,7 @@ describe('<OrganizationsList />', () => {
});
test('api is called to delete Orgs for each org in selected.', () => {
const fetchOrganizations = jest.fn(() => wrapper.find('OrganizationsList').setState({
organizations: []
}));
wrapper = mountWithContexts(
<OrganizationsList
fetchOrganizations={fetchOrganizations}
/>, {
context: { network: { api } }
}
);
wrapper = mountWithContexts(<OrganizationsList />);
const component = wrapper.find('OrganizationsList');
wrapper.find('OrganizationsList').setState({
organizations: mockAPIOrgsList.data.results,
@@ -130,14 +107,10 @@ describe('<OrganizationsList />', () => {
expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(component.state('selected').length);
});
test('call fetchOrganizations after org(s) have been deleted', () => {
const fetchOrgs = jest.spyOn(_OrganizationsList.prototype, 'fetchOrganizations');
test('call loadOrganizations after org(s) have been deleted', () => {
const fetchOrgs = jest.spyOn(_OrganizationsList.prototype, 'loadOrganizations');
const event = { preventDefault: () => { } };
wrapper = mountWithContexts(
<OrganizationsList />, {
context: { network: { api } }
}
);
wrapper = mountWithContexts(<OrganizationsList />);
wrapper.find('OrganizationsList').setState({
organizations: mockAPIOrgsList.data.results,
itemCount: 3,
@@ -153,13 +126,9 @@ describe('<OrganizationsList />', () => {
const history = createMemoryHistory({
initialEntries: ['organizations?order_by=name&page=1&page_size=5'],
});
const handleError = jest.fn();
wrapper = mountWithContexts(
<OrganizationsList />, {
context: {
router: { history }, network: { api, handleHttpError: handleError }
}
}
<OrganizationsList />,
{ context: { router: { history } } }
);
await wrapper.setState({
organizations: mockAPIOrgsList.data.results,
@@ -173,6 +142,5 @@ describe('<OrganizationsList />', () => {
wrapper.update();
const component = wrapper.find('OrganizationsList');
component.instance().handleOrgDelete();
expect(handleError).toHaveBeenCalled();
});
});

View File

@@ -1,21 +1,11 @@
import React from 'react';
import { mountWithContexts } from '../../enzymeHelpers';
import { mountWithContexts, waitForElement } from '../../enzymeHelpers';
import TemplatesList, { _TemplatesList } from '../../../src/pages/Templates/TemplatesList';
import { UnifiedJobTemplatesAPI } from '../../../src/api';
jest.mock('../../../src/api');
const setDefaultState = (templatesList) => {
templatesList.setState({
itemCount: mockUnifiedJobTemplatesFromAPI.length,
isLoading: false,
isInitialized: true,
selected: [],
templates: mockUnifiedJobTemplatesFromAPI,
});
templatesList.update();
};
const mockUnifiedJobTemplatesFromAPI = [{
const mockTemplates = [{
id: 1,
name: 'Template 1',
url: '/templates/job_template/1',
@@ -47,6 +37,19 @@ const mockUnifiedJobTemplatesFromAPI = [{
}];
describe('<TemplatesList />', () => {
beforeEach(() => {
UnifiedJobTemplatesAPI.read.mockResolvedValue({
data: {
count: mockTemplates.length,
results: mockTemplates
}
});
});
afterEach(() => {
jest.clearAllMocks();
});
test('initially renders succesfully', () => {
mountWithContexts(
<TemplatesList
@@ -55,46 +58,33 @@ describe('<TemplatesList />', () => {
/>
);
});
test('Templates are retrieved from the api and the components finishes loading', async (done) => {
const readTemplates = jest.spyOn(_TemplatesList.prototype, 'readUnifiedJobTemplates');
const wrapper = mountWithContexts(<TemplatesList />).find('TemplatesList');
expect(wrapper.state('isLoading')).toBe(true);
await expect(readTemplates).toHaveBeenCalled();
wrapper.update();
expect(wrapper.state('isLoading')).toBe(false);
const loadUnifiedJobTemplates = jest.spyOn(_TemplatesList.prototype, 'loadUnifiedJobTemplates');
const wrapper = mountWithContexts(<TemplatesList />);
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === true);
expect(loadUnifiedJobTemplates).toHaveBeenCalled();
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
done();
});
test('handleSelect is called when a template list item is selected', async () => {
test('handleSelect is called when a template list item is selected', async (done) => {
const handleSelect = jest.spyOn(_TemplatesList.prototype, 'handleSelect');
const wrapper = mountWithContexts(<TemplatesList />);
const templatesList = wrapper.find('TemplatesList');
setDefaultState(templatesList);
expect(templatesList.state('isLoading')).toBe(false);
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
wrapper.find('DataListCheck#select-jobTemplate-1').props().onChange();
expect(handleSelect).toBeCalled();
templatesList.update();
expect(templatesList.state('selected').length).toBe(1);
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('selected').length === 1);
done();
});
test('handleSelectAll is called when a template list item is selected', async () => {
test('handleSelectAll is called when a template list item is selected', async (done) => {
const handleSelectAll = jest.spyOn(_TemplatesList.prototype, 'handleSelectAll');
const wrapper = mountWithContexts(<TemplatesList />);
const templatesList = wrapper.find('TemplatesList');
setDefaultState(templatesList);
expect(templatesList.state('isLoading')).toBe(false);
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
wrapper.find('Checkbox#select-all').props().onChange(true);
expect(handleSelectAll).toBeCalled();
wrapper.update();
expect(templatesList.state('selected').length).toEqual(templatesList.state('templates')
.length);
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('selected').length === 3);
done();
});
});