diff --git a/client/src/components/BackupCodes/tests/Register-test.js b/client/src/components/BackupCodes/tests/Register-test.js index 842d7a0c..8bec2312 100644 --- a/client/src/components/BackupCodes/tests/Register-test.js +++ b/client/src/components/BackupCodes/tests/Register-test.js @@ -1,127 +1,57 @@ -/* global jest */ +/* global jest, test */ import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import Register from '../Register'; - -Enzyme.configure({ adapter: new Adapter() }); +import { render, fireEvent, screen } from '@testing-library/react'; window.ss = { i18n: { _t: (key, string) => string }, }; -const mockMethod = { - urlSegment: 'aye', - name: 'Aye', - description: 'Register using aye', - supportLink: 'https://google.com', - component: 'Test', -}; - -describe('Register - Recovery Codes', () => { - it('will show a recently copied message when using the copy test button', () => { - const preventDefault = jest.fn(); - - const wrapper = shallow( - - ); - - const copyLink = wrapper.find('.mfa-register-backup-codes__copy-to-clipboard'); - - expect(copyLink.text()).toBe('Copy codes'); - copyLink.simulate('click', { preventDefault }); - - expect(preventDefault.mock.calls).toHaveLength(1); - - wrapper.update(); - - expect(wrapper.find('.mfa-register-backup-codes__copy-to-clipboard').text()).toBe('Copied!'); - }); - - it('will hide the recently copied message after a short delay', done => { - const preventDefault = jest.fn(); - - const wrapper = shallow( - - ); - - const copyLink = wrapper.find('.mfa-register-backup-codes__copy-to-clipboard'); - - expect(copyLink.text()).toBe('Copy codes'); - copyLink.simulate('click', { preventDefault }); - - expect(preventDefault.mock.calls).toHaveLength(1); - - wrapper.update(); - - expect(wrapper.find('.mfa-register-backup-codes__copy-to-clipboard').text()).toBe('Copied!'); - - setTimeout(() => { - expect(wrapper.find('.mfa-register-backup-codes__copy-to-clipboard').text()) - .toBe('Copy codes'); - done(); - }, 40); - }); - - it('re-copying the codes will reset the recently copied timer', done => { - const preventDefault = jest.fn(); - - const wrapper = shallow( - - ); - - const copyLink = wrapper.find('.mfa-register-backup-codes__copy-to-clipboard'); - - expect(copyLink.text()).toBe('Copy codes'); - copyLink.simulate('click', { preventDefault }); - - expect(preventDefault.mock.calls).toHaveLength(1); - - wrapper.update(); - - expect(wrapper.find('.mfa-register-backup-codes__copy-to-clipboard').text()).toBe('Copied!'); - - setTimeout(() => { - expect(wrapper.find('.mfa-register-backup-codes__copy-to-clipboard').text()) - .toBe('Copied!'); - copyLink.simulate('click', { preventDefault }); - expect(preventDefault.mock.calls).toHaveLength(2); - wrapper.update(); - }, 150); - - setTimeout(() => { - expect(wrapper.find('.mfa-register-backup-codes__copy-to-clipboard').text()) - .toBe('Copied!'); - done(); - }, 400); - }); - - it('will call the given onComplete function when pressing the "finish" button', () => { - const completeFunction = jest.fn(); - - const wrapper = shallow( - - ); - - wrapper.find('button.btn-primary').simulate('click'); +window.prompt = () => {}; + +function makeProps(obj = {}) { + return { + method: { + urlSegment: 'aye', + name: 'Aye', + description: 'Register using aye', + supportLink: 'https://google.com', + component: 'Test', + }, + codes: ['123', '456'], + copyFeedbackDuration: 30, + ...obj + }; +} + +test('Register will show a recently copied message when using the copy test button and hide after a short delay', async () => { + const { container } = render(); + let link = container.querySelector('.mfa-register-backup-codes__copy-to-clipboard'); + expect(link.textContent).toBe('Copy codes'); + fireEvent.click(link); + link = await screen.findByText('Copied!'); + expect(link.classList).toContain('mfa-register-backup-codes__copy-to-clipboard'); + expect(screen.queryByText('Copy codes!')).toBeNull(); + link = await screen.findByText('Copy codes'); + expect(link.classList).toContain('mfa-register-backup-codes__copy-to-clipboard'); + expect(screen.queryByText('Copied!')).toBeNull(); + // Can do this multiple times + fireEvent.click(link); + link = await screen.findByText('Copied!'); + expect(link).not.toBeNull(); + link = await screen.findByText('Copy codes'); + expect(link).not.toBeNull(); +}); - expect(completeFunction.mock.calls).toHaveLength(1); - }); +test('Register will call the given onComplete function when pressing the "finish" button', () => { + const onCompleteRegistration = jest.fn(); + const { container } = render( + + ); + fireEvent.click(container.querySelector('button.btn-primary')); + expect(onCompleteRegistration).toHaveBeenCalled(); }); diff --git a/client/src/components/BackupCodes/tests/Verify-test.js b/client/src/components/BackupCodes/tests/Verify-test.js index 92925189..af7e95dc 100644 --- a/client/src/components/BackupCodes/tests/Verify-test.js +++ b/client/src/components/BackupCodes/tests/Verify-test.js @@ -1,56 +1,45 @@ -/* global jest */ +/* global jest, test */ import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import Verify from '../Verify'; - -Enzyme.configure({ adapter: new Adapter() }); +import { render, fireEvent } from '@testing-library/react'; window.ss = { i18n: { _t: (key, string) => string }, }; -describe('Login - Recovery Codes', () => { - it('has a disabled button on load', () => { - const wrapper = shallow(); - - expect(wrapper.find('button').prop('disabled')).toBe(true); - }); - - it('will un-disable the button when input is provided', () => { - const wrapper = shallow(); - - wrapper.setState({ - value: 'something', - }); - wrapper.update(); - - expect(wrapper.find('button').prop('disabled')).toBe(false); - }); - - it('renders the given "more options control"', () => { - const moreOptions =
More options!
; - - const wrapper = shallow(); - - expect(wrapper.html()).toMatch(/
More options!<\/div>/); - }); - - it('triggers login completion with the right value when the button is pressed', () => { - const completeFunction = jest.fn(); - const preventDefault = jest.fn(); - - const wrapper = shallow(); +test('Verify has a disabled button on load', () => { + const { container } = render(); + expect(container.querySelector('button.btn-primary').disabled).toBe(true); +}); - wrapper.setState({ - value: 'something', - }); +test('Verify will un-disable the button when input is provided', () => { + const { container } = render(); + const input = container.querySelector('input.mfa-verify-backup-codes__input'); + fireEvent.change(input, { target: { value: 'x' } }); + expect(container.querySelector('button.btn-primary').disabled).toBe(false); +}); - wrapper.find('button').simulate('click', { preventDefault }); +test('Verify renders the given "more options control"', () => { + const { container } = render( + More options!
+ }} + /> + ); + expect(container.querySelectorAll('.mfa-action-list__item')[1].textContent).toBe('More options!'); +}); - expect(preventDefault.mock.calls).toHaveLength(1); - expect(completeFunction.mock.calls).toHaveLength(1); - expect(completeFunction.mock.calls[0]).toEqual([{ code: 'something' }]); - }); +test('Verify triggers login completion with the right value when the button is pressed', () => { + const onCompleteVerification = jest.fn(); + const { container } = render( + + ); + const input = container.querySelector('input.mfa-verify-backup-codes__input'); + fireEvent.change(input, { target: { value: 'something' } }); + fireEvent.click(container.querySelector('button.btn-primary')); + expect(onCompleteVerification).toBeCalledWith({ code: 'something' }); }); diff --git a/client/src/components/FormField/RegisteredMFAMethodListField/AccountResetUI.js b/client/src/components/FormField/RegisteredMFAMethodListField/AccountResetUI.js index 8bccfddb..ce54ad74 100644 --- a/client/src/components/FormField/RegisteredMFAMethodListField/AccountResetUI.js +++ b/client/src/components/FormField/RegisteredMFAMethodListField/AccountResetUI.js @@ -105,11 +105,12 @@ class AccountResetUI extends Component { */ renderSending() { const { ss: { i18n } } = window; + const { LoadingIndicatorComponent } = this.props; return (

- + { @@ -232,4 +233,8 @@ AccountResetUI.propTypes = { resetEndpoint: PropTypes.string, }; +AccountResetUI.defaultProps = { + LoadingIndicatorComponent: LoadingIndicator, +}; + export default AccountResetUI; diff --git a/client/src/components/FormField/RegisteredMFAMethodListField/MethodListItem.js b/client/src/components/FormField/RegisteredMFAMethodListField/MethodListItem.js index 42e2a838..d6b9da32 100644 --- a/client/src/components/FormField/RegisteredMFAMethodListField/MethodListItem.js +++ b/client/src/components/FormField/RegisteredMFAMethodListField/MethodListItem.js @@ -51,13 +51,13 @@ class MethodListItem extends PureComponent { } renderRemove() { - const { canRemove, method } = this.props; + const { canRemove, method, RemoveComponent } = this.props; if (!canRemove) { return null; } - return ; + return ; } renderReset() { @@ -104,13 +104,13 @@ class MethodListItem extends PureComponent { * @returns {SetDefault} */ renderSetAsDefault() { - const { isDefaultMethod, isBackupMethod, method } = this.props; + const { isDefaultMethod, isBackupMethod, method, SetDefaultComponent } = this.props; if (isDefaultMethod || isBackupMethod) { return null; } - return ; + return ; } @@ -123,9 +123,9 @@ class MethodListItem extends PureComponent { return (

- { this.renderRemove() } - { this.renderReset() } - { this.renderSetAsDefault() } + { this.renderRemove() } + { this.renderReset() } + { this.renderSetAsDefault() }
); } @@ -156,8 +156,8 @@ class MethodListItem extends PureComponent { return ( - { this.renderNameAndStatus() } - { this.renderControls() } + { this.renderNameAndStatus() } + { this.renderControls() } ); } @@ -182,6 +182,8 @@ MethodListItem.defaultProps = { canRemove: false, canReset: false, tag: 'li', + RemoveComponent: Remove, + SetDefaultComponent: SetDefault }; export default MethodListItem; diff --git a/client/src/components/FormField/RegisteredMFAMethodListField/RegisteredMFAMethodListField.js b/client/src/components/FormField/RegisteredMFAMethodListField/RegisteredMFAMethodListField.js index cfc3a8c9..647f9abb 100644 --- a/client/src/components/FormField/RegisteredMFAMethodListField/RegisteredMFAMethodListField.js +++ b/client/src/components/FormField/RegisteredMFAMethodListField/RegisteredMFAMethodListField.js @@ -110,7 +110,7 @@ class RegisteredMFAMethodListField extends Component { * @return {MethodListItem|null} */ renderBackupMethod() { - const { backupMethod, backupCreatedDate, registeredMethods, readOnly } = this.props; + const { backupMethod, backupCreatedDate, registeredMethods, readOnly, MethodListItemComponent } = this.props; if (!backupMethod) { return null; @@ -126,7 +126,7 @@ class RegisteredMFAMethodListField extends Component { } return ( - { @@ -162,7 +162,7 @@ class RegisteredMFAMethodListField extends Component { canReset: !readOnly, }; - return ; + return ; }); } @@ -177,10 +177,11 @@ class RegisteredMFAMethodListField extends Component { backupMethod, endpoints, resources, + RegisterModalComponent } = this.props; return ( - ({ + __esModule: true, + default: (endpoint, method, body, headers) => { + lastApiCallArgs = { endpoint, method, body, headers }; + return new Promise((resolve, reject) => { + resolveApiCall = resolve; + rejectApiCall = reject; + }); + } +})); -import confirm from 'reactstrap-confirm'; +window.ss = { + i18n: { _t: (key, string) => string }, +}; jest.mock('reactstrap-confirm', () => jest.fn().mockImplementation( () => Promise.resolve(true) )); -Enzyme.configure({ adapter: new Adapter() }); - -window.ss = { - i18n: { _t: (key, string) => string }, -}; - -const fetchMock = jest.spyOn(global, 'fetch'); +function makeProps(obj = {}) { + return { + // this is mocked to prevent a "
cannot appear as a descendant of

" warning + LoadingIndicatorComponent: () => loading, + ...obj + }; +} + +test('AccountResetUI is disabled when an endpoint has not been supplied', () => { + const { container } = render(); + expect(container.querySelector('.account-reset-action .btn').disabled).toBe(true); +}); -describe('AccountResetUI', () => { - beforeEach(() => { - fetchMock.mockImplementation(() => Promise.resolve({ - status: 200, - json: () => Promise.resolve({ success: true }), - })); +test('AccountResetUI is enabled when an endpoint has been supplied', () => { + const { container } = render( + + ); + expect(container.querySelector('.account-reset-action .btn').disabled).toBe(false); +}); - fetchMock.mockClear(); - confirm.mockClear(); +test('AccountResetUI submits the reset request when clicked and hides the button', async () => { + const { container } = render( + + ); + fireEvent.click(container.querySelector('.account-reset-action .btn')); + await screen.findByText('Sending...'); + expect(container.querySelector('.account-reset-action .btn')).toBeNull(); + resolveApiCall({ + json: () => Promise.resolve({}) }); - - describe('renderAction()', () => { - it('is disabled when an endpoint has not been supplied', () => { - const ui = shallow( - - ); - - const action = ui.find('.account-reset-action .btn'); - - expect(action).toHaveLength(1); - expect(action.first().props().disabled).toBeTruthy(); - }); - - it('is enabled when an endpoint has been supplied', () => { - const ui = shallow( - - ); - - const action = ui.find('.account-reset-action .btn'); - - expect(action).toHaveLength(1); - expect(action.first().props().disabled).toBeFalsy(); - }); - - it('submits the reset request when clicked', done => { - const ui = shallow( - - ); - - ui.find('.account-reset-action .btn').first().simulate('click'); - - setTimeout(() => { - expect(fetchMock.mock.calls.length).toBe(1); - done(); - }); - }); - - it('is hidden when submitting or complete', () => { - const ui = shallow( - - ); - - ui.instance().setState({ - submitting: true, - }); - - let action = ui.find('.account-reset-action .btn'); - - expect(action).toHaveLength(0); - - ui.instance().setState({ - submitting: false, - complete: true, - }); - - action = ui.find('.account-reset-action .btn'); - - expect(action).toHaveLength(0); - }); + expect(lastApiCallArgs).toStrictEqual({ + body: '{"csrf_token":"SecurityID"}', + endpoint: '/reset/1', + headers: undefined, + method: 'POST' }); + await screen.findByText('An email has been sent.'); +}); - describe('renderStatusMessage()', () => { - it('does not display a status by default', () => { - const ui = shallow( - - ); - - expect(ui.find('.account-reset-action__message')).toHaveLength(0); - }); - - it('displays a sending status when the form is submitted', () => { - const ui = shallow( - - ); - - ui.instance().setState({ - submitting: true, - }); - - const message = ui.find('.account-reset-action__message'); - - expect(message).toHaveLength(1); - expect(message.text()).toContain('Sending...'); - }); - - it('displays an error status when the request fails', () => { - const ui = shallow( - - ); - - ui.instance().setState({ - complete: true, - failed: true, - }); - - const message = ui.find('.account-reset-action__message'); - - expect(message).toHaveLength(1); - expect(message.text()).toContain('unable to send an email'); - }); - - it('displays a complete status when the request succeeds', () => { - const ui = shallow( - - ); - - ui.instance().setState({ - complete: true, - failed: false, - }); - - const message = ui.find('.account-reset-action__message'); +test('AccountResetUI does not display a status by default', () => { + const { container } = render(); + expect(container.querySelector('.account-reset-action__message')).toBeNull(); +}); - expect(message).toHaveLength(1); - expect(message.text()).toContain('email has been sent'); - }); - }); +test('AccountResetUI displays an error status when the request fails', async () => { + const { container } = render( + + ); + fireEvent.click(container.querySelector('.account-reset-action .btn')); + await screen.findByText('Sending...'); + rejectApiCall(); + await screen.findByText('We were unable to send an email, please try again later.'); }); diff --git a/client/src/components/FormField/RegisteredMFAMethodListField/tests/MethodListItem-test.js b/client/src/components/FormField/RegisteredMFAMethodListField/tests/MethodListItem-test.js index 585a34ea..177af018 100644 --- a/client/src/components/FormField/RegisteredMFAMethodListField/tests/MethodListItem-test.js +++ b/client/src/components/FormField/RegisteredMFAMethodListField/tests/MethodListItem-test.js @@ -1,11 +1,7 @@ -/* global jest, describe, it, expect */ +/* global jest, test, describe, it, expect */ import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import MethodListItem from '../MethodListItem'; -import Remove from '../MethodListItem/Remove'; - -Enzyme.configure({ adapter: new Adapter() }); +import { render } from '@testing-library/react'; window.ss = { i18n: { @@ -15,49 +11,50 @@ window.ss = { }, }; -describe('MethodListitem', () => { - describe('getStatusMessage()', () => { - it('identifies default methods', () => { - const wrapper = shallow( - - ); - - expect(wrapper.instance().getStatusMessage()).toContain('(default)'); - }); - - it('identifies backup methods', () => { - const wrapper = shallow( - - ); +function makeProps(obj = {}) { + return { + method: { + urlSegment: 'foo' + }, + RemoveComponent: () =>

, + SetDefaultComponent: () =>
, + ...obj + }; +} + +test('MethodListitem identifies default methods', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-method-list-item').textContent).toBe('{method} (default): Registered'); +}); - expect(wrapper.instance().getStatusMessage()).toContain('Created'); - }); - }); - describe('render()', () => { - it('does not render remove buttons by default', () => { - const wrapper = shallow( - - ); +test('MethodListItem identifies backup methods', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-method-list-item').textContent).toBe('{method}: Created {date}'); +}); - expect(wrapper.find(Remove)).toHaveLength(0); - }); - it('does render remove buttons if canRemove is true', () => { - const wrapper = shallow( - - ); +test('MethodListItem does not render remove buttons by default', () => { + const { container } = render( + + ); + expect(container.querySelector('.test-remove')).toBeNull(); +}); - expect(wrapper.find(Remove)).toHaveLength(1); - }); - }); +test('MethodListItem does render remove buttons if canRemove is true', () => { + const { container } = render( + + ); + expect(container.querySelector('.test-remove')).not.toBeNull(); }); diff --git a/client/src/components/FormField/RegisteredMFAMethodListField/tests/RegisteredMFAMethodListField-test.js b/client/src/components/FormField/RegisteredMFAMethodListField/tests/RegisteredMFAMethodListField-test.js index 27a1943b..fd7553fc 100644 --- a/client/src/components/FormField/RegisteredMFAMethodListField/tests/RegisteredMFAMethodListField-test.js +++ b/client/src/components/FormField/RegisteredMFAMethodListField/tests/RegisteredMFAMethodListField-test.js @@ -1,186 +1,106 @@ -/* global jest, describe, it, expect */ +/* global jest, test, describe, it, expect */ import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import { Component as RegisteredMFAMethodListField } from '../RegisteredMFAMethodListField'; -import { loadComponent } from 'lib/Injector'; // eslint-disable-line - -Enzyme.configure({ adapter: new Adapter() }); +import { render } from '@testing-library/react'; window.ss = { - i18n: { _t: (key, string) => string }, + i18n: { _t: (key, string) => string, detectLocale: () => 'en', inject: () => {} }, }; -const altMethod = { name: 'Method', urlSegment: 'method', component: '' }; -const backupMethod = { ...altMethod, name: 'Backup Method', urlSegment: 'backup' }; -const defaultMethod = { ...altMethod, name: 'Default Method', urlSegment: 'default' }; -const defaultMethodName = 'default'; - -const RegisterComponent = () =>
; -const onUpdateAvailableMethods = jest.fn(); -const onSetRegisteredMethods = jest.fn(); -const onSetDefaultMethod = jest.fn(); +const altMethod = { name: 'Alt Method', urlSegment: 'altmethod', component: '' }; +const backupMethod = { name: 'Backup Method', urlSegment: 'backup', component: '' }; +const defaultMethod = { name: 'Default Method', urlSegment: 'default', component: '' }; + +function makeProps(obj = {}) { + return { + defaultMethod: 'default', + backupMethod, + availableMethods: [], + registeredMethods: [], + RegisterModalComponent: () =>
, + MethodListItemComponent: ({ method }) =>
, + onUpdateAvailableMethods: () => {}, + onSetRegisteredMethods: () => {}, + onSetDefaultMethod: () => {}, + ...obj + }; +} import translationStrings from '../../../../../lang/src/en.json'; -describe('RegisteredMFAMethodListField', () => { - describe('baseMethods()', () => { - it('filters out backup methods', () => { - const registeredMethods = [altMethod, backupMethod, defaultMethod]; - - const field = shallow( - - ); - - expect(field.instance().getBaseMethods()).toHaveLength(2); - }); - }); - - describe('renderAddButton', () => { - it('renders a button', () => { - const availableMethods = [altMethod]; - - const wrapper = shallow( - - ); - - expect(wrapper.find('.registered-mfa-method-list-field__button')).toHaveLength(1); - }); - - it('doesn\'t render a button in read-only mode', () => { - const availableMethods = [altMethod]; - - const wrapper = shallow( - - ); - - expect(wrapper.find('.registered-mfa-method-list-field__button')).toHaveLength(0); - }); - - - it('provides a contextual message depending on registered methods', () => { - const availableMethods = [altMethod]; - - const withoutRegisteredMethods = shallow( - - ); - - expect(withoutRegisteredMethods - .find('.registered-mfa-method-list-field__button') - .shallow() - .text() - ).toBe(translationStrings['MultiFactorAuthentication.ADD_FIRST_METHOD']); - - const withRegisteredMethods = shallow( - - ); - - expect(withRegisteredMethods - .find('.registered-mfa-method-list-field__button') - .shallow() - .text() - ).toBe(translationStrings['MultiFactorAuthentication.ADD_ANOTHER_METHOD']); - }); - }); - - describe('render()', () => { - it('renders the read-only view when readOnly is passed', () => { - const registeredMethods = [altMethod]; - - const field = shallow( - - ); - +test('RegisteredMFAMethodListField filters out backup methods', () => { + const { container } = render( + + ); + const methods = container.querySelectorAll('.method-list .test-method-list-item'); + expect(methods).toHaveLength(2); + expect(methods[0].getAttribute('title')).toBe('altmethod'); + expect(methods[1].getAttribute('title')).toBe('default'); +}); - expect(field.hasClass('registered-mfa-method-list-field--read-only')).toEqual(true); - }); +test('RegisteredMFAMethodListField renders a button when there are available methods', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-mfa-method-list-field__button')).not.toBeNull(); +}); - it('renders a button when there are registerable methods', () => { - const availableMethods = [altMethod]; +test('RegisteredMFAMethodListField does not render a button when there are no available methods', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-mfa-method-list-field__button')).toBeNull(); +}); - const withAvailableMethods = shallow( - - ); +test('RegisteredMFAMethodListField doesn\'t render a button in read-only mode', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-mfa-method-list-field__button')).toBeNull(); +}); - expect(withAvailableMethods.find('.registered-mfa-method-list-field__button')) - .toHaveLength(1); +test('RegisteredMFAMethodListField renders a button with the correct label', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-mfa-method-list-field__button').textContent).toBe(translationStrings['MultiFactorAuthentication.ADD_FIRST_METHOD']); +}); - const withoutAvailableMethods = shallow( - - ); +test('RegisteredMFAMethodListField renders a button with the correct label when there are already registered methods', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-mfa-method-list-field__button').textContent).toBe(translationStrings['MultiFactorAuthentication.ADD_ANOTHER_METHOD']); +}); - expect(withoutAvailableMethods.find('.registered-mfa-method-list-field__button')) - .toHaveLength(0); - }); - }); +test('RegisteredMFAMethodListField renders the read-only view when readOnly is passed', () => { + const { container } = render( + + ); + expect(container.querySelector('.registered-mfa-method-list-field--read-only')).not.toBeNull(); }); diff --git a/client/src/components/Register.js b/client/src/components/Register.js index 74e23271..2304bb30 100644 --- a/client/src/components/Register.js +++ b/client/src/components/Register.js @@ -212,10 +212,10 @@ class Register extends Component { * @return {Introduction} */ renderIntroduction() { - const { canSkip, resources, endpoints: { skip }, showSubTitle } = this.props; + const { canSkip, resources, endpoints: { skip }, showSubTitle, IntroductionComponent } = this.props; return ( - - { showSubTitle && } + { showSubTitle && <TitleComponent /> } <RegistrationComponent {...registerProps} method={selectedMethod} @@ -264,10 +264,10 @@ class Register extends Component { * @return {SelectMethod|null} */ renderOptions() { - const { availableMethods, showSubTitle } = this.props; + const { availableMethods, showSubTitle, SelectMethodComponent } = this.props; return ( - <SelectMethod + <SelectMethodComponent methods={availableMethods} showTitle={showSubTitle} /> @@ -275,12 +275,12 @@ class Register extends Component { } render() { - const { screen, onCompleteRegistration, showTitle, showSubTitle, completeMessage } = this.props; + const { screen, onCompleteRegistration, showTitle, showSubTitle, completeMessage, CompleteComponent } = this.props; const { ss: { i18n } } = window; if (screen === SCREEN_COMPLETE) { return ( - <Complete + <CompleteComponent showTitle={showSubTitle} onComplete={onCompleteRegistration} message={completeMessage} @@ -337,6 +337,10 @@ Register.defaultProps = { showTitle: true, showSubTitle: true, showIntroduction: true, + IntroductionComponent: Introduction, + SelectMethodComponent: SelectMethod, + CompleteComponent: Complete, + TitleComponent: Title }; const mapStateToProps = state => { diff --git a/client/src/components/Register/Introduction.js b/client/src/components/Register/Introduction.js index df2b0bdf..cc5a5af7 100644 --- a/client/src/components/Register/Introduction.js +++ b/client/src/components/Register/Introduction.js @@ -32,12 +32,12 @@ export const ActionList = ({ canSkip, onContinue, onSkip }) => { ); }; -const Introduction = ({ canSkip, onContinue, onSkip, resources, showTitle }) => { +const Introduction = ({ canSkip, onContinue, onSkip, resources, showTitle, TitleComponent }) => { const { ss: { i18n } } = window; return ( <div> - { showTitle && <Title /> } + { showTitle && <TitleComponent /> } <h4 className="mfa-feature-list-title"> { i18n._t('MultiFactorAuthentication.HOW_IT_WORKS', fallbacks['MultiFactorAuthentication.HOW_IT_WORKS']) } @@ -131,6 +131,7 @@ Introduction.propTypes = { Introduction.defaultProps = { showTitle: true, + TitleComponent: Title }; export { Introduction as Component }; diff --git a/client/src/components/Register/SelectMethod.js b/client/src/components/Register/SelectMethod.js index 848d26a9..2ce2a98f 100644 --- a/client/src/components/Register/SelectMethod.js +++ b/client/src/components/Register/SelectMethod.js @@ -103,7 +103,7 @@ class SelectMethod extends Component { } render() { - const { methods, showTitle } = this.props; + const { methods, showTitle, TitleComponent, MethodTileComponent } = this.props; const { highlightedMethod } = this.state; const classes = classnames('mfa-method-tile-group', { @@ -112,11 +112,11 @@ class SelectMethod extends Component { return ( <div> - {showTitle && <Title />} + {showTitle && <TitleComponent />} <ul className={classes}> {methods.map(method => ( - <MethodTile + <MethodTileComponent isActive={highlightedMethod === method} key={method.urlSegment} method={method} @@ -142,6 +142,8 @@ SelectMethod.propTypes = { SelectMethod.defaultProps = { showTitle: true, + TitleComponent: Title, + MethodTileComponent: MethodTile }; const mapDispatchToProps = dispatch => ({ diff --git a/client/src/components/Register/tests/Introduction-test.js b/client/src/components/Register/tests/Introduction-test.js index e3ab12e9..3b75436d 100644 --- a/client/src/components/Register/tests/Introduction-test.js +++ b/client/src/components/Register/tests/Introduction-test.js @@ -1,119 +1,86 @@ -/* global jest, describe, it, expect */ +/* global jest, test, describe, it, expect */ -// eslint-disable-next-line no-unused-vars -import fetch from 'isomorphic-fetch'; import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import { Component as Introduction, ActionList } from '../Introduction'; -import { loadComponent } from 'lib/Injector'; // eslint-disable-line - -Enzyme.configure({ adapter: new Adapter() }); +import { fireEvent, render } from '@testing-library/react'; window.ss = { i18n: { _t: (key, string) => string }, }; -const fetchMock = jest.spyOn(global, 'fetch'); -const handleContinueMock = jest.fn(() => true); -const handleSkipMock = jest.fn(() => true); - -describe('Introduction', () => { - beforeEach(() => { - fetchMock.mockImplementation(() => Promise.resolve({ - status: 200, - json: () => Promise.resolve({}), - })); - - fetchMock.mockClear(); - handleContinueMock.mockClear(); - handleSkipMock.mockClear(); - }); - - describe('render()', () => { - it('renders images when resource URLs are supplied', () => { - const wrapper = shallow( - <Introduction - onContinue={handleContinueMock} - resources={{ - extra_factor_image_url: '#', - unique_image_url: '#', - }} - /> - ); - - const images = wrapper.find('img'); - - expect(images).toHaveLength(2); - }); - - it('renders "find out more" link when user docs URL is supplied', () => { - const wrapper = shallow( - <Introduction - onContinue={handleContinueMock} - resources={{ - user_help_link: '#', - }} - /> - ); - - const images = wrapper.find('a'); - - expect(images).toHaveLength(1); - }); - }); - - describe('ActionList', () => { - it('does not render a skip button by default', () => { - const wrapper = shallow( - <ActionList - onContinue={handleContinueMock} - /> - ); - - const actionList = wrapper.find('button'); - - expect(actionList).toHaveLength(1); - }); - - it('triggers the continue handler when the continue action is clicked', () => { - const wrapper = shallow( - <ActionList - onContinue={handleContinueMock} - /> - ); - - wrapper.find('button').first().simulate('click'); - - expect(handleContinueMock.mock.calls.length).toBe(1); - }); - - it('renders a skip button when supplied', () => { - const wrapper = shallow( - <ActionList - canSkip - onContinue={handleContinueMock} - onSkip={handleContinueMock} - /> - ); +function makeProps(obj = {}) { + return { + onContinue: () => {}, + TitleComponent: () => <div className="test-title" />, + ...obj + }; +} + +test('Introduction renders images when resource URLs are supplied', () => { + const { container } = render( + <Introduction {...makeProps({ + resources: { + extra_factor_image_url: '/path/to/extra-factor.png', + unique_image_url: '/unique.png', + } + })} + /> + ); + const images = container.querySelectorAll('img.mfa-feature-list-item__icon'); + expect(images[0].getAttribute('src')).toBe('/path/to/extra-factor.png'); + expect(images[1].getAttribute('src')).toBe('/unique.png'); +}); - const actionList = wrapper.find('button'); +test('Introduction renders "find out more" link when user docs URL is supplied', () => { + const { container } = render( + <Introduction {...makeProps({ + resources: { + user_help_link: '/help-link', + } + })} + /> + ); + expect(container.querySelector('.mfa-feature-list-item__description a').getAttribute('href')).toBe('/help-link'); +}); - expect(actionList).toHaveLength(2); - }); +test('ActionList does not render a skip button by default', () => { + const { container } = render( + <ActionList /> + ); + expect(container.querySelector('.btn-primary').textContent).toBe('Get started'); +}); - it('triggers the skip handler when the skip action is clicked', () => { - const wrapper = shallow( - <ActionList - canSkip - onContinue={handleContinueMock} - onSkip={handleSkipMock} - /> - ); +test('ActionList triggers the continue handler when the continue action is clicked', () => { + const onContinue = jest.fn(); + const { container } = render( + <ActionList {...{ + onContinue + }} + /> + ); + fireEvent.click(container.querySelector('.btn-primary')); + expect(onContinue).toHaveBeenCalled(); +}); - wrapper.find('button').last().simulate('click'); +test('ActionList renders a skip button when supplied', () => { + const { container } = render( + <ActionList {...{ + canSkip: true + }} + /> + ); + expect(container.querySelector('.btn-secondary').textContent).toBe('Setup later'); +}); - expect(handleSkipMock.mock.calls.length).toBe(1); - }); - }); +test('ActionList triggers the skip handler when the skip action is clicked', () => { + const onSkip = jest.fn(); + const { container } = render( + <ActionList {...{ + canSkip: true, + onSkip + }} + /> + ); + fireEvent.click(container.querySelector('.btn-secondary')); + expect(onSkip).toHaveBeenCalled(); }); diff --git a/client/src/components/Register/tests/MethodTile-test.js b/client/src/components/Register/tests/MethodTile-test.js index a50e469e..1caac9ff 100644 --- a/client/src/components/Register/tests/MethodTile-test.js +++ b/client/src/components/Register/tests/MethodTile-test.js @@ -1,11 +1,25 @@ -/* global jest, describe, it, expect */ +/* global jest, test, describe, it, expect */ import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import { Component as MethodTile } from '../MethodTile'; +import { fireEvent, render } from '@testing-library/react'; -Enzyme.configure({ adapter: new Adapter() }); +function makeProps(obj = {}) { + return { + method: { + urlSegment: 'aye', + name: 'Aye', + description: 'Register using aye', + supportLink: 'https://google.com', + component: 'Test', + isAvailable: true + }, + onClick: () => {}, + isAvailable: () => true, + getUnavailableMessage: () => {}, + ...obj + }; +} window.ss = { i18n: { @@ -14,123 +28,69 @@ window.ss = { }, }; -const firstMethod = { - urlSegment: 'aye', - name: 'Aye', - description: 'Register using aye', - supportLink: 'https://google.com', - component: 'Test', - isAvailable: true, -}; - -const clickHandlerMock = jest.fn(); -const isAvailableMock = () => true; -const getUnavailableMessageMock = () => 'Testing'; - -describe('MethodTile', () => { - beforeEach(() => { - clickHandlerMock.mockClear(); - }); - - describe('handleClick()', () => { - it('passes click to handler prop if method is available', () => { - firstMethod.isAvailable = true; - const wrapper = shallow( - <MethodTile - method={firstMethod} - onClick={clickHandlerMock} - isAvailable={isAvailableMock} - getUnavailableMessage={getUnavailableMessageMock} - /> - ); - wrapper.instance().handleClick({}); - expect(clickHandlerMock.mock.calls).toHaveLength(1); - }); - - it('doesn\'t do anything when method is not available', () => { - firstMethod.isAvailable = false; - const wrapper = shallow( - <MethodTile - method={firstMethod} - onClick={clickHandlerMock} - isAvailable={isAvailableMock} - getUnavailableMessage={getUnavailableMessageMock} - /> - ); - wrapper.instance().handleClick({}); - expect(clickHandlerMock.mock.calls).toHaveLength(0); - }); - }); +test('MethodTile passes click to handler prop if method is available', () => { + const onClick = jest.fn(); + const { container } = render( + <MethodTile {...makeProps({ + onClick + })} + /> + ); + fireEvent.click(container.querySelector('.mfa-method-tile__content')); + expect(onClick).toHaveBeenCalled(); +}); - describe('renderUnavailableMask()', () => { - it('does nothing when the method is available', () => { - firstMethod.isAvailable = true; - const wrapper = shallow( - <MethodTile - method={firstMethod} - onClick={clickHandlerMock} - isAvailable={isAvailableMock} - getUnavailableMessage={getUnavailableMessageMock} - /> - ); - expect(wrapper.find('.mfa-method-tile__unavailable-mask')).toHaveLength(0); - }); +test('MethodTile click doesn\t do anything when method not available', () => { + const onClick = jest.fn(); + const { container } = render( + <MethodTile {...makeProps({ + method: { + ...makeProps().method, + isAvailable: false + } + })} + /> + ); + fireEvent.click(container.querySelector('.mfa-method-tile__content')); + expect(onClick).not.toHaveBeenCalled(); +}); - it('renders a mask with message via props when unavailable', () => { - const wrapper = shallow( - <MethodTile - method={firstMethod} - onClick={clickHandlerMock} - isAvailable={() => false} - getUnavailableMessage={() => 'Test message here'} - /> - ); - const mask = wrapper.find('.mfa-method-tile__unavailable-mask'); - expect(mask).toHaveLength(1); - expect(mask.text()).toContain('Test message here'); - }); - }); +test('MethodTile renders does not render a mask when is available', () => { + const { container } = render( + <MethodTile {...makeProps()}/> + ); + expect(container.querySelector('.mfa-method-tile__unavailable-mask')).toBeNull(); +}); - describe('render()', () => { - it('has a clickable interface', () => { - firstMethod.isAvailable = true; - const wrapper = shallow( - <MethodTile - method={firstMethod} - onClick={clickHandlerMock} - isAvailable={isAvailableMock} - getUnavailableMessage={getUnavailableMessageMock} - /> - ); - wrapper.find('.mfa-method-tile__content').simulate('click'); - expect(clickHandlerMock.mock.calls).toHaveLength(1); - }); +test('MethodTile renders a mask when is not available', () => { + const { container } = render( + <MethodTile {...makeProps({ + isAvailable: () => false, + getUnavailableMessage: () => 'Test message here' + })} + /> + ); + expect(container.querySelector('.mfa-method-tile__unavailable-mask').textContent).toBe('Unsupported: Test message here'); +}); - it('treats the enter key as a click', () => { - firstMethod.isAvailable = true; - const wrapper = shallow( - <MethodTile - method={firstMethod} - onClick={clickHandlerMock} - isAvailable={isAvailableMock} - getUnavailableMessage={getUnavailableMessageMock} - /> - ); - wrapper.find('.mfa-method-tile__content').simulate('keyUp', { keyCode: 13 }); - expect(clickHandlerMock.mock.calls).toHaveLength(1); - }); +test('MethodTile treats the enter key as a click', () => { + const onClick = jest.fn(); + const { container } = render( + <MethodTile {...makeProps({ + onClick + })} + /> + ); + fireEvent.keyUp(container.querySelector('.mfa-method-tile__content'), { keyCode: 13 }); + expect(onClick).toHaveBeenCalled(); +}); - it('attaches an active state when active', () => { - const wrapper = shallow( - <MethodTile - isActive - method={firstMethod} - onClick={clickHandlerMock} - isAvailable={isAvailableMock} - getUnavailableMessage={getUnavailableMessageMock} - /> - ); - expect(wrapper.find('.mfa-method-tile--active')).toHaveLength(1); - }); - }); +test('MethodTile attaches an active state when active', () => { + const { container } = render( + <MethodTile {...makeProps({ + isActive: true + })} + /> + ); + expect(container.querySelector('.mfa-method-tile--active')).not.toBeNull(); }); diff --git a/client/src/components/Register/tests/SelectMethod-test.js b/client/src/components/Register/tests/SelectMethod-test.js index d3dbdc76..2c648ebf 100644 --- a/client/src/components/Register/tests/SelectMethod-test.js +++ b/client/src/components/Register/tests/SelectMethod-test.js @@ -1,147 +1,125 @@ -/* global jest, describe, it, expect */ +/* global jest, test, describe, it, expect */ import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import { Component as SelectMethod } from '../SelectMethod'; - -jest.mock('../MethodTile'); - -Enzyme.configure({ adapter: new Adapter() }); +import { render, fireEvent } from '@testing-library/react'; window.ss = { i18n: { _t: (key, string) => string }, }; -const selectMethodMock = jest.fn(); - -const firstMethod = { - urlSegment: 'aye', - name: 'Aye', - description: 'Register using aye', - supportLink: 'https://google.com', - component: 'Test', -}; - -describe('SelectMethod', () => { - it('automatically selects the only available method', () => { - const mockNextHandler = jest.fn(); - shallow( - <SelectMethod - methods={[firstMethod]} - isAvailable={() => true} - onSelectMethod={mockNextHandler} - /> - ); - - expect(mockNextHandler).toHaveBeenCalledTimes(1); - }); - - it('does not automatically select the only available method when not usable', () => { - const mockNextHandler = jest.fn(); - shallow( - <SelectMethod - methods={[firstMethod]} - isAvailable={() => false} - onSelectMethod={mockNextHandler} - /> - ); - - expect(mockNextHandler).toHaveBeenCalledTimes(0); - }); +function makeProps(obj = {}) { + return { + methods: [ + { + urlSegment: 'aye', + name: 'Aye', + description: 'Register using aye', + supportLink: 'https://google.com', + component: 'Test', + }, + { + urlSegment: 'bee', + name: 'Bee', + description: 'Register using bee', + supportLink: 'https://foo.test', + component: 'Test', + }, + ], + isAvailable: () => true, + onClickBack: () => null, + onSelectMethod: () => null, + TitleComponent: () => <div className="test-title" />, + MethodTileComponent: ({ method, onClick }) => <div className="test-method-tile" data-method={method.urlSegment} onClick={onClick} />, + ...obj + }; +} + +test('SelectMethod automatically selects the only available method', () => { + const onSelectMethod = jest.fn(); + render( + <SelectMethod {...makeProps({ + methods: [ + makeProps().methods[0] + ], + onSelectMethod + })} + /> + ); + expect(onSelectMethod).toHaveBeenCalled(); +}); - describe('handleGoToNext()', () => { - it('passes the highlighted method to the method selection handler', () => { - const wrapper = shallow( - <SelectMethod - methods={[]} - onSelectMethod={(method) => { - expect(method.description).toBe('my mock method'); - }} - /> - ); - wrapper.setState({ highlightedMethod: { description: 'my mock method' } }); - wrapper.instance().handleGoToNext(); - }); - }); +test('SelectMethod does not automatically select the only available method when not usable', () => { + const onSelectMethod = jest.fn(); + render( + <SelectMethod {...makeProps({ + onSelectMethod, + methods: [ + makeProps().methods[0] + ], + isAvailable: () => false + })} + /> + ); + expect(onSelectMethod).not.toHaveBeenCalled(); +}); - describe('handleBack()', () => { - it('has a todo alert', () => { - window.alert = (message) => { - expect(message).toContain('Todo'); - }; - const wrapper = shallow( - <SelectMethod - methods={[]} - onSelectMethod={selectMethodMock} - /> - ); - wrapper.instance().handleBack(); - }); +test('SelectMethod passes the highlighted method to the onSelectMethod handler', async () => { + const onSelectMethod = jest.fn(); + const { container } = render( + <SelectMethod {...makeProps({ + onSelectMethod + })} + /> + ); + fireEvent.click(container.querySelector('[data-method="bee"]')); + fireEvent.click(container.querySelector('.mfa-action-list__item .btn-primary')); + expect(onSelectMethod).toHaveBeenCalledWith({ + component: 'Test', + description: 'Register using bee', + name: 'Bee', + supportLink: 'https://foo.test', + urlSegment: 'bee' }); +}); - describe('renderActions()', () => { - it('renders a "Next" button', () => { - const wrapper = shallow( - <SelectMethod - methods={[]} - onSelectMethod={selectMethodMock} - /> - ); - - const button = wrapper.find('.mfa-action-list .btn').first(); - expect(button.text()).toBe('Next'); - }); - - it('renders a "Next" button in a disabled state when no method is highlighted', () => { - const wrapper = shallow( - <SelectMethod - methods={[]} - onSelectMethod={selectMethodMock} - /> - ); - - const button = wrapper.find('.mfa-action-list .btn').first(); - expect(button.props().disabled).toBe(true); - }); - - it('renders an active "Next" button when a method is highlighted', () => { - const wrapper = shallow( - <SelectMethod - methods={[firstMethod]} - onSelectMethod={selectMethodMock} - /> - ); - wrapper.instance().handleClick(firstMethod); +test('SelectMethod clicking the back button triggers the onClickBack callback', () => { + const onClickBack = jest.fn(); + const { container } = render( + <SelectMethod {...makeProps({ + onClickBack + })} + /> + ); + fireEvent.click(container.querySelector('.mfa-action-list__item .btn-secondary')); + expect(onClickBack).toHaveBeenCalled(); +}); - const button = wrapper.find('.mfa-action-list .btn').first(); - expect(button.props().disabled).toBe(false); - }); +test('SelectMethod renders a "Next" button', () => { + const { container } = render(<SelectMethod {...makeProps()}/>); + expect(container.querySelector('.mfa-action-list .btn-primary').textContent).toBe('Next'); +}); - it('renders a "Back" button', () => { - const wrapper = shallow( - <SelectMethod - methods={[]} - onSelectMethod={selectMethodMock} - /> - ); +test('SelectMethod renders a "Next" button in a disabled state when no method is highlighted', () => { + const { container } = render(<SelectMethod {...makeProps()}/>); + expect(container.querySelector('.mfa-action-list .btn-primary').disabled).toBe(true); +}); - const button = wrapper.find('.mfa-action-list .btn').at(1); - expect(button.text()).toBe('Back'); - }); - }); +test('SelectMethod renders an active "Next" button when a method is highlighted', () => { + const { container } = render(<SelectMethod {...makeProps()}/>); + fireEvent.click(container.querySelector('[data-method="bee"]')); + expect(container.querySelector('.mfa-action-list .btn-primary').disabled).toBe(false); +}); - describe('render()', () => { - it('renders a MethodTile component for each available method', () => { - const wrapper = shallow( - <SelectMethod - methods={[firstMethod]} - onSelectMethod={selectMethodMock} - /> - ); +test('SelectMethod renders a "Back" button', () => { + const { container } = render(<SelectMethod {...makeProps()}/>); + expect(container.querySelector('.mfa-action-list .btn-secondary').textContent).toBe('Back'); +}); - expect(wrapper.find('.mfa-method-tile-group')).toHaveLength(1); - expect(wrapper.find('.mfa-method-tile-group').children()).toHaveLength(1); - }); - }); +test('SelectMethod renders a MethodTile component for each available method', () => { + const { container } = render(<SelectMethod {...makeProps()}/>); + const methodTiles = container.querySelectorAll('.test-method-tile'); + expect(methodTiles).toHaveLength(2); + expect(methodTiles[0].getAttribute('data-method')).toBe('aye'); + expect(methodTiles[1].getAttribute('data-method')).toBe('bee'); }); diff --git a/client/src/components/Verify.js b/client/src/components/Verify.js index 7d245129..161304bd 100644 --- a/client/src/components/Verify.js +++ b/client/src/components/Verify.js @@ -270,7 +270,7 @@ class Verify extends Component { renderOtherMethods() { const otherMethods = this.getOtherMethods(); const { selectedMethod, showOtherMethods } = this.state; - const { resources } = this.props; + const { resources, SelectMethodComponenet } = this.props; if (selectedMethod && !showOtherMethods) { return null; @@ -279,7 +279,7 @@ class Verify extends Component { return ( <Fragment> {this.renderTitle()} - <SelectMethod + <SelectMethodComponenet resources={resources} methods={otherMethods} onClickBack={this.handleHideOtherMethodsPane} @@ -383,6 +383,10 @@ Verify.propTypes = { defaultMethod: PropTypes.string, }; +Verify.defaultProps = { + SelectMethodComponenet: SelectMethod +}; + export { Verify as Component }; export default withMethodAvailability(Verify); diff --git a/client/src/components/Verify/tests/SelectMethod-test.js b/client/src/components/Verify/tests/SelectMethod-test.js index 23a2912b..8715a531 100644 --- a/client/src/components/Verify/tests/SelectMethod-test.js +++ b/client/src/components/Verify/tests/SelectMethod-test.js @@ -1,12 +1,9 @@ -/* global jest, describe, it, expect */ +/* global jest, test, expect */ // eslint-disable-next-line no-unused-vars import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import { Component as SelectMethod } from '../SelectMethod'; - -Enzyme.configure({ adapter: new Adapter() }); +import { render, fireEvent } from '@testing-library/react'; window.ss = { i18n: { @@ -18,113 +15,101 @@ window.ss = { }, }; -const mockRegisteredMethods = [ - { - urlSegment: 'aye', - name: 'aye', - component: 'Test', - }, - { - urlSegment: 'bee', - name: 'bee', - component: 'Test', - }, -]; - -const mockClickHandler = jest.fn(); -const mockSelectMethodHandler = jest.fn(); - -describe('Verify', () => { - beforeEach(() => { - mockClickHandler.mockClear(); - mockSelectMethodHandler.mockClear(); - }); - - describe('renderControls()', () => { - it('shows a back button that takes you back', () => { - const wrapper = shallow( - <SelectMethod - isAvailable={() => true} - onClickBack={mockClickHandler} - onSelectMethod={mockSelectMethodHandler} - getUnavailableMessage={() => ''} - methods={mockRegisteredMethods} - /> - ); - - expect(wrapper.find('.mfa-verify-select-method__actions')).toHaveLength(1); - expect(wrapper.find('.mfa-verify-select-method__back')).toHaveLength(1); - - wrapper.find('.mfa-verify-select-method__back').simulate('click'); - expect(mockClickHandler).toHaveBeenCalled(); - }); - }); - - describe('renderMethod()', () => { - it('shows methods as unavailable', () => { - const wrapper = shallow( - <SelectMethod - isAvailable={() => false} - onClickBack={mockClickHandler} - onSelectMethod={mockSelectMethodHandler} - getUnavailableMessage={() => 'Browser does not support it'} - methods={mockRegisteredMethods} - /> - ); - - const wrapperText = wrapper.text(); - expect(wrapperText).toContain(mockRegisteredMethods[0].name); - expect(wrapperText).toContain('Browser does not support it'); - }); +const firstMethod = { + urlSegment: 'aye', + name: 'Aye', + description: 'Register using aye', + supportLink: 'https://google.com', + component: 'TestRegistration', +}; +const secondMethod = { + urlSegment: 'bee', + name: 'Bee', + description: 'Register using bee', + supportLink: '', + component: 'TestRegistration', +}; - it('shows methods as available', () => { - const wrapper = shallow( - <SelectMethod - isAvailable={() => true} - onClickBack={mockClickHandler} - onSelectMethod={mockSelectMethodHandler} - getUnavailableMessage={() => ''} - methods={mockRegisteredMethods} - /> - ); +function makeProps(obj = {}) { + return { + isAvailable: () => true, + onClickBack: () => null, + onSelectMethod: () => null, + getUnavailableMessage: () => '', + methods: [firstMethod, secondMethod], + ...obj + }; +} + +test('Verify renderControls() shows a back button that takes you back', () => { + const onClickBack = jest.fn(); + const { container } = render( + <SelectMethod {...makeProps({ + onClickBack, + })} + /> + ); + expect(container.querySelectorAll('.mfa-verify-select-method__actions')).toHaveLength(1); + expect(container.querySelectorAll('.mfa-verify-select-method__back')).toHaveLength(1); + fireEvent.click(container.querySelector('.mfa-verify-select-method__back')); + expect(onClickBack).toHaveBeenCalled(); +}); - expect(wrapper.text()).toContain(mockRegisteredMethods[0].name); - expect(wrapper.find('li a')).toHaveLength(2); - }); +test('Verify renderMethod() shows methods as unavailable', () => { + const { container } = render( + <SelectMethod {...makeProps({ + isAvailable: () => false, + getUnavailableMessage: () => 'Browser does not support it', + })} + /> + ); + expect(container.querySelectorAll('.mfa-verify-select-method__method')).toHaveLength(2); + expect(container.querySelectorAll('.mfa-verify-select-method__method--unavailable')).toHaveLength(2); + expect(container.querySelectorAll('.mfa-verify-select-method__method--available')).toHaveLength(0); + expect(container.querySelectorAll('.mfa-verify-select-method__method-message')[0].textContent).toContain('Browser does not support it'); +}); - it('triggers click handler when clicking a method', () => { - const wrapper = shallow( - <SelectMethod - isAvailable={() => true} - onClickBack={mockClickHandler} - onSelectMethod={mockSelectMethodHandler} - getUnavailableMessage={() => ''} - methods={mockRegisteredMethods} - /> - ); +test('Verify renderMethod() shows methods as available', () => { + const { container } = render( + <SelectMethod {...makeProps({ + isAvailable: () => true, + })} + /> + ); + expect(container.querySelectorAll('.mfa-verify-select-method__method--unavailable')).toHaveLength(0); + const methods = container.querySelectorAll('.mfa-verify-select-method__method'); + expect(methods).toHaveLength(2); + expect(methods[0].textContent).toBe('Verify with {aye}'); + expect(methods[1].textContent).toBe('Verify with {bee}'); +}); - const firstMethod = wrapper.find('li a').first(); - firstMethod.simulate('click'); - expect(mockSelectMethodHandler).toHaveBeenCalled(); - }); +test('Verify renderMethod() trigggers click handler when clicking a method', () => { + const onSelectMethod = jest.fn(); + const { container } = render( + <SelectMethod {...makeProps({ + onSelectMethod, + })} + /> + ); + const method = container.querySelectorAll('.mfa-verify-select-method__method')[0]; + fireEvent.click(method); + expect(onSelectMethod).toHaveBeenCalledWith({ + component: 'TestRegistration', + description: 'Register using aye', + name: 'Aye', + supportLink: 'https://google.com', + urlSegment: 'aye', }); +}); - describe('render()', () => { - it('renders an image', () => { - const wrapper = shallow( - <SelectMethod - isAvailable={() => true} - onClickBack={mockClickHandler} - onSelectMethod={mockSelectMethodHandler} - getUnavailableMessage={() => ''} - methods={mockRegisteredMethods} - resources={{ - more_options_image_url: '/foo.jpg', - }} - /> - ); - - expect(wrapper.find('.mfa-verify-select-method__image')).toHaveLength(1); - }); - }); +test('Verify render() renders an image', () => { + const { container } = render( + <SelectMethod {...makeProps({ + resources: { + more_options_image_url: '/foo.jpg', + } + })} + /> + ); + expect(container.querySelectorAll('.mfa-verify-select-method__image')).toHaveLength(1); }); diff --git a/client/src/components/tests/Register-test.js b/client/src/components/tests/Register-test.js index de251baa..2373a535 100644 --- a/client/src/components/tests/Register-test.js +++ b/client/src/components/tests/Register-test.js @@ -1,485 +1,311 @@ -/* global jest, describe, it, expect */ +/* global jest, test, describe, it, expect */ -import Complete from '../Register/Complete'; - -jest.mock('lib/Injector'); -jest.mock('../Register/SelectMethod'); - -// eslint-disable-next-line no-unused-vars -import fetch from 'isomorphic-fetch'; import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import { Component as Register, SCREEN_REGISTER_METHOD, SCREEN_CHOOSE_METHOD, SCREEN_COMPLETE } from '../Register'; -import SelectMethod from '../Register/SelectMethod'; -import Introduction from '../Register/Introduction'; -import { loadComponent } from 'lib/Injector'; // eslint-disable-line - -Enzyme.configure({ adapter: new Adapter() }); +import { render, screen, fireEvent, act } from '@testing-library/react'; + +let lastApiCallArgs; +let resolveApiCall; +let rejectApiCall; + +jest.mock('lib/api', () => ({ + __esModule: true, + default: (endpoint, method, body, headers) => { + lastApiCallArgs = { endpoint, method, body, headers }; + return new Promise((resolve, reject) => { + resolveApiCall = resolve; + rejectApiCall = reject; + }); + } +})); + +jest.mock('lib/Injector', () => ({ + __esModule: true, + loadComponent: (component) => { + if (component === 'TestRegistration') { + return ({ method, onBack, onCompleteRegistration }) => ( + <div data-testid="test-registration" data-method={method.urlSegment} onClick={() => onCompleteRegistration({ my: 'regodata' })}> + <input data-testid="test-back" onClick={onBack}/> + </div> + ); + } + return null; + } +})); window.ss = { i18n: { _t: (key, string) => string }, }; -const endpoints = { - register: '/fake/{urlSegment}', - skip: '/fake/skip', - complete: '/fake/complete', -}; - const firstMethod = { urlSegment: 'aye', name: 'Aye', description: 'Register using aye', supportLink: 'https://google.com', - component: 'Test', + component: 'TestRegistration', +}; +const secondMethod = { + urlSegment: 'bee', + name: 'Bee', + description: 'Register using bee', + supportLink: '', + component: 'TestRegistration', }; -const mockAvailableMethods = [ - firstMethod, - { - urlSegment: 'bee', - name: 'Bee', - description: 'Register using bee', - supportLink: '', - component: 'Test', - }, -]; - -const fetchMock = jest.spyOn(global, 'fetch'); -const onCompleteRegistration = jest.fn(); -const onRemoveAvailableMethod = jest.fn(); -describe('Register', () => { - beforeEach(() => { - fetchMock.mockImplementation(() => Promise.resolve({ - status: 200, - json: () => Promise.resolve({}), - })); - fetchMock.mockClear(); - loadComponent.mockClear(); - onCompleteRegistration.mockClear(); +function makeProps(obj = {}) { + return { + endpoints: { + register: '/fake/{urlSegment}', + skip: '/fake/skip', + complete: '/fake/complete', + }, + screen: SCREEN_REGISTER_METHOD, + availableMethods: [firstMethod, secondMethod], + registeredMethods: [], + selectedMethod: firstMethod, + onSelectMethod: () => null, + backupMethod: null, + canSkip: true, + onCompleteRegistration: () => null, + onRemoveAvailableMethod: () => null, + IntroductionComponent: ({ canSkip }) => ( + <div data-testid="test-introduction"> + {canSkip && <div data-testid="test-skip"/>} + </div> + ), + TitleComponent: () => <div className="test-title"/>, + SelectMethodComponent: ({ methods }) => ( + <div className="test-select-method"> + {methods.map((method) => <div key={method.name} data-testid="test-method">{method.description}</div>)} + </div> + ), + CompleteComponent: ({ onComplete }) => ( + <div data-testid="test-complete"> + <input data-testid="test-complete-button" onClick={onComplete}/> + </div> + ), + ...obj, + }; +} + +test('Register sets the selected method as the backup method', async () => { + let doResolve; + const promise = new Promise((resolve) => { + doResolve = resolve; }); - - describe('setupBackupMethod()', () => { - it('sets the selected method as the backup method', () => { - const onSelectMethod = jest.fn(); - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - registeredMethods={[]} // will cause "should set up backup methods" to be true - selectedMethod={firstMethod} - backupMethod={{ - name: 'Test', - }} - onSelectMethod={onSelectMethod} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - // Run the handler and check that it's changed to the backup method - wrapper.instance().setupBackupMethod(); - expect(onSelectMethod).toHaveBeenCalledWith({ name: 'Test' }); - }); - - it('clears the selected method and sets to be completed', () => { - const onSelectMethod = jest.fn(); - const onShowComplete = jest.fn(); - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - registeredMethods={[]} - backupMethod={null} - selectedMethod={firstMethod} - onSelectMethod={onSelectMethod} - onShowComplete={onShowComplete} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - // Run the handler and check that it's changed to the backup method - wrapper.instance().setupBackupMethod(); - expect(onSelectMethod).not.toHaveBeenCalled(); - expect(onShowComplete).toHaveBeenCalled(); - }); + const onSelectMethod = jest.fn(() => doResolve()); + render( + <Register {...makeProps({ + backupMethod: { + name: 'Test' + }, + onSelectMethod + })} + /> + ); + // api call is triggered as part of componentDidMount() + resolveApiCall({ + json: () => Promise.resolve({ + SecurityID: 'foo', + registerProps: { + bar: 'baz' + } + }) }); - - describe('clearRegistrationErrors()', () => { - it('clears registration errors and registration props', () => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - registeredMethods={[]} - onCompleteRegistration={onCompleteRegistration} - onShowChooseMethod={() => {}} - /> - ); - - wrapper.instance().setState({ - registerProps: { - foo: 'bar', - error: 'I haven\'nt had my coffee yet!', - }, - }); - wrapper.instance().handleBack(); - expect(wrapper.instance().state.registerProps).toBeNull(); - }); + const registration = await screen.findByTestId('test-registration'); + // it this test the onClick handler triggers the onCompleteRegistration handler + fireEvent.click(registration); + resolveApiCall({ + status: 201, }); + // Prevent the "Warning: An update to Register inside a test was not wrapped in act(...)" warning + /* eslint-disable-next-line no-return-await */ + await act(async () => await promise); + expect(onSelectMethod).toHaveBeenCalledWith({ name: 'Test' }); +}); - describe('fetchStartRegistrationData', () => { - it('is called when the selected method is changed', done => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - registeredMethods={[]} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - expect(fetchMock.mock.calls).toHaveLength(0); - - wrapper.setProps({ - selectedMethod: firstMethod, - }, () => { - expect(fetchMock.mock.calls).toHaveLength(1); - expect(fetchMock.mock.calls[0][0]).toBe('/fake/aye'); - done(); - }); - }); - - it('stores the response in state', done => { - const mockResponse = { - myProp: 1, - anotherProp: 'two', - }; - fetchMock.mockImplementation(() => Promise.resolve({ - json: () => Promise.resolve(mockResponse), - })); - - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - registeredMethods={[]} - selectedMethod={firstMethod} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - setTimeout(() => { - expect(wrapper.state('registerProps')).toEqual(mockResponse); - done(); - }); - }); - - it('stores a CSRF token separately', done => { - const mockResponse = { - myProp: 1, - anotherProp: 'two', - }; - fetchMock.mockImplementation(() => Promise.resolve({ - json: () => Promise.resolve({ ...mockResponse, SecurityID: 'test' }), - })); - - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - registeredMethods={[]} - selectedMethod={firstMethod} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - setTimeout(() => { - expect(wrapper.state('registerProps')).toEqual(mockResponse); - expect(wrapper.state('token')).toBe('test'); - done(); - }); - }); +test('Register clears the selected method and sets to be completed', async () => { + let doResolve; + const promise = new Promise((resolve) => { + doResolve = resolve; }); - - describe('handleBack()', () => { - it('unselects the selected method', () => { - const onShowChooseMethod = jest.fn(); - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - registeredMethods={[]} - onShowChooseMethod={onShowChooseMethod} - selectedMethod={firstMethod} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - wrapper.instance().handleBack(); - expect(onShowChooseMethod).toHaveBeenCalled(); - }); + const onSelectMethod = jest.fn(); + const onShowComplete = jest.fn(() => doResolve()); + render( + <Register {...makeProps({ + onSelectMethod, + onShowComplete + })} + /> + ); + resolveApiCall({ + json: () => Promise.resolve({ + SecurityID: 'foo', + registerProps: { + bar: 'baz' + } + }) }); - - describe('handleCompleteRegistration()', () => { - it('will call the "start" endpoint when a method is chosen', done => { - const wrapper = shallow( - <Register - endpoints={endpoints} - selectedMethod={firstMethod} - availableMethods={mockAvailableMethods} - onCompleteRegistration={onCompleteRegistration} - />, - { disableLifecycleMethods: true } - ); - wrapper.instance().handleCompleteRegistration({ myData: 'foo' }); - - setTimeout(() => { - expect(fetchMock.mock.calls).toHaveLength(1); - const firstCallJson = JSON.stringify(fetchMock.mock.calls[0]); - expect(firstCallJson).toContain('/fake/aye'); - expect(firstCallJson).toContain('myData'); - expect(firstCallJson).toContain('foo'); - done(); - }); - }); + const registration = await screen.findByTestId('test-registration'); + fireEvent.click(registration); + resolveApiCall({ + status: 201, }); + /* eslint-disable-next-line no-return-await */ + await act(async () => await promise); + expect(onSelectMethod).not.toHaveBeenCalled(); + expect(onShowComplete).toHaveBeenCalled(); +}); - describe('renderMethod()', () => { - it('will load the component for the chosen method', done => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - selectedMethod={firstMethod} - screen={SCREEN_REGISTER_METHOD} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - setTimeout(() => { - // "Test" is the name of the specified component, see the definition for "firstMethod" - expect(wrapper.find('Test')).toHaveLength(1); - done(); - }); - }); - - it('forwards API response as props to injected component', (done) => { - fetchMock.mockImplementation(() => Promise.resolve({ - json: () => Promise.resolve({ - myProp: 1, - anotherProp: 'two', - }), - })); - - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - selectedMethod={firstMethod} - screen={SCREEN_REGISTER_METHOD} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - setTimeout(() => { - // "Test" is the name of the specified component, see the definition for "firstMethod" - expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ - myProp: 1, - anotherProp: 'two', - })); - done(); - }); - }); - - it('provides the current method definition to the injected component', (done) => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - selectedMethod={firstMethod} - screen={SCREEN_REGISTER_METHOD} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - setTimeout(() => { - // "Test" is the name of the specified component, see the definition for "firstMethod" - expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ - method: firstMethod, - })); - done(); - }); - }); - - it('calls the API when the complete function is called', done => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - selectedMethod={firstMethod} - screen={SCREEN_REGISTER_METHOD} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - setTimeout(() => { - expect(fetchMock.mock.calls).toHaveLength(1); - // "Test" is the name of the specified component, see the definition for "firstMethod" - const completeFunction = wrapper.find('Test').prop('onCompleteRegistration'); - completeFunction({ test: 1 }); - expect(fetchMock.mock.calls).toHaveLength(2); - expect(fetchMock.mock.calls[1]).toEqual(['/fake/aye', { - credentials: 'same-origin', - method: 'POST', - headers: {}, - body: '{"test":1}', - }]); - done(); - }); - }); - - it('shows the complete screen when registration is complete', done => { - const onShowComplete = jest.fn(); - - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - selectedMethod={firstMethod} - screen={SCREEN_REGISTER_METHOD} - onCompleteRegistration={onCompleteRegistration} - onShowComplete={onShowComplete} - onRemoveAvailableMethod={onRemoveAvailableMethod} - /> - ); - - setTimeout(() => { - // Expect that a "start" has already been requested - expect(fetchMock.mock.calls).toHaveLength(1); - - // Mock the next response as a 201 (Created) - fetchMock.mockReturnValueOnce(Promise.resolve({ - status: 201, - json: () => Promise.resolve({}), - })); - - // Trigger a complete on the registration method component - // "Test" is the name of the specified component, see the definition for "firstMethod" - const completeFunction = wrapper.find('Test').prop('onCompleteRegistration'); - completeFunction({ test: 1 }); - expect(fetchMock.mock.calls).toHaveLength(2); - - setTimeout(() => { - expect(onShowComplete).toHaveBeenCalled(); - done(); - }); - }); - }); +test('Register calls the API with the correct params', async () => { + let doResolve; + const promise = new Promise((resolve) => { + doResolve = resolve; }); - - describe('renderOptions()', () => { - it('renders a SelectMethod with available methods to register passed', () => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - screen={SCREEN_CHOOSE_METHOD} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - const listItems = wrapper.find(SelectMethod); - const methods = listItems.props().methods; - - expect(methods).toHaveLength(2); - expect(methods[0].description).toMatch(/Register using aye/); - expect(methods[1].description).toMatch(/Register using bee/); - }); + const onShowComplete = jest.fn(() => doResolve()); + render( + <Register {...makeProps({ + onShowComplete + })} + /> + ); + resolveApiCall({ + json: () => Promise.resolve({ + SecurityID: 'foo', + registerProps: { + bar: 'baz' + } + }) }); - - describe('renderIntroduction()', () => { - it('renders the Introduction UI on first load', () => { - const wrapper = shallow( - <Register - canSkip - endpoints={endpoints} - availableMethods={mockAvailableMethods} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - const introElement = wrapper.find(Introduction); - expect(introElement).toHaveLength(1); - }); - - it('indicates introduction cannot skip if the skip endpoint is not defined', () => { - const customEndpoints = { - ...endpoints, - skip: null, - }; - - const wrapper = shallow( - <Register - canSkip - endpoints={customEndpoints} - availableMethods={mockAvailableMethods} - onCompleteRegistration={onCompleteRegistration} - /> - ); - - const introElement = wrapper.find(Introduction); - expect(introElement.props().canSkip).toBeFalsy(); - }); + expect(lastApiCallArgs.endpoint).toBe('/fake/aye'); + expect(lastApiCallArgs.method).toBe(undefined); + expect(lastApiCallArgs.body).toBe(undefined); + const registration = await screen.findByTestId('test-registration'); + fireEvent.click(registration); + resolveApiCall({ + status: 201, }); + /* eslint-disable-next-line no-return-await */ + await act(async () => await promise); + expect(onShowComplete).toHaveBeenCalled(); + expect(lastApiCallArgs.endpoint).toBe('/fake/aye?SecurityID=foo'); + expect(lastApiCallArgs.method).toBe('POST'); + expect(lastApiCallArgs.body).toBe('{"my":"regodata"}'); +}); - describe('render()', () => { - it('renders the complete screen', () => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - onCompleteRegistration={onCompleteRegistration} - screen={SCREEN_COMPLETE} - /> - ); +test('Register calls the onShowChooseMethod callback on back', async () => { + let doResolve; + const promise = new Promise((resolve) => { + doResolve = resolve; + }); + const onShowChooseMethod = jest.fn(() => doResolve()); + render( + <Register {...makeProps({ + onShowChooseMethod, + })} + /> + ); + resolveApiCall({ + json: () => Promise.resolve({}) + }); + const backButton = await screen.findByTestId('test-back'); + fireEvent.click(backButton); + /* eslint-disable-next-line no-return-await */ + await act(async () => await promise); + expect(onShowChooseMethod).toHaveBeenCalled(); +}); - expect(wrapper.find(Complete)).toHaveLength(1); - }); +test('Register will load the component for the chosen method', async () => { + render( + <Register {...makeProps()}/> + ); + resolveApiCall({ + json: () => Promise.resolve({}) + }); + const registration = await screen.findByTestId('test-registration'); + expect(registration.getAttribute('data-method')).toBe('aye'); +}); - it('renders the complete screen', () => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - onCompleteRegistration={onCompleteRegistration} - screen={SCREEN_COMPLETE} - /> - ); +test('Register renders a SelectMethod with available methods to register passed', async () => { + render( + <Register {...makeProps({ + screen: SCREEN_CHOOSE_METHOD + })} + /> + ); + resolveApiCall({ + json: () => Promise.resolve({}) + }); + await screen.findByText('Register using aye'); + const methods = screen.getAllByTestId('test-method'); + expect(methods[0].textContent).toBe('Register using aye'); + expect(methods[1].textContent).toBe('Register using bee'); +}); - expect(wrapper.find(Complete)).toHaveLength(1); - }); +test('Register renders the Introduction UI on first load', async () => { + render( + <Register {...makeProps({ + screen: null + })} + /> + ); + const intro = await screen.findByTestId('test-introduction'); + expect(intro).not.toBeNull(); +}); - it('renders a button in complete that will trigger onCompleteRegistration', () => { - const wrapper = shallow( - <Register - endpoints={endpoints} - availableMethods={mockAvailableMethods} - onCompleteRegistration={onCompleteRegistration} - screen={SCREEN_COMPLETE} - /> - ); +test('Register indicates introduction can skip if the skip endpoint is defined', async () => { + render( + <Register {...makeProps({ + screen: null, + })} + /> + ); + await screen.findByTestId('test-introduction'); + expect(screen.queryByTestId('test-skip')).not.toBeNull(); +}); - const completeWrapper = wrapper.find(Complete).shallow(); +test('Register indicates introduction cannot skip if the skip endpoint is not defined', async () => { + render( + <Register {...makeProps({ + screen: null, + endpoints: { + ...makeProps().endpoints, + skip: null + } + })} + /> + ); + await screen.findByTestId('test-introduction'); + expect(screen.queryByTestId('test-skip')).toBeNull(); +}); - completeWrapper.find('button').simulate('click'); +test('Register renders the complete screen', async () => { + render( + <Register {...makeProps({ + screen: SCREEN_COMPLETE, + })} + /> + ); + const complete = await screen.findByTestId('test-complete'); + expect(complete).not.toBeNull(); +}); - expect(onCompleteRegistration).toHaveBeenCalled(); - }); - }); +test('Register renders a button in complete that triggers onCompleteRegistration', async () => { + const onCompleteRegistration = jest.fn(); + render( + <Register {...makeProps({ + screen: SCREEN_COMPLETE, + onCompleteRegistration + })} + /> + ); + await screen.findByTestId('test-complete'); + const completeButton = screen.getByTestId('test-complete-button'); + fireEvent.click(completeButton); + expect(onCompleteRegistration).toHaveBeenCalled(); }); diff --git a/client/src/components/tests/Verify-test.js b/client/src/components/tests/Verify-test.js index 143e9121..a3bd8603 100644 --- a/client/src/components/tests/Verify-test.js +++ b/client/src/components/tests/Verify-test.js @@ -1,18 +1,44 @@ -/* global jest, describe, it, expect */ +/* global jest, test, describe, it, expect */ -jest.mock('lib/Injector'); - -// eslint-disable-next-line no-unused-vars -import fetch from 'isomorphic-fetch'; import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import { Component as Verify } from '../Verify'; -import SelectMethod from '../Verify/SelectMethod'; -import LoadingError from 'components/LoadingError'; -import { loadComponent } from 'lib/Injector'; // eslint-disable-line - -Enzyme.configure({ adapter: new Adapter() }); +import { render, screen, fireEvent } from '@testing-library/react'; + +let lastApiCallArgs; +let resolveApiCall; +let rejectApiCall; + +jest.mock('lib/api', () => ({ + __esModule: true, + default: (endpoint, method, body, headers) => { + lastApiCallArgs = { endpoint, method, body, headers }; + return new Promise((resolve, reject) => { + resolveApiCall = resolve; + rejectApiCall = reject; + }); + } +})); + +let lastInjectorLoadComponentArg; + +jest.mock('lib/Injector', () => ({ + __esModule: true, + loadComponent: (component) => { + lastInjectorLoadComponentArg = component; + return (props) => { + const dataProps = Object.keys(props).map(k => `${k}=${props[k]}`).join(','); + return <div + data-component={component} + data-props={dataProps} + onClick={() => props.onCompleteVerification({ + my: 'verifydata' + })} + > + {props.moreOptionsControl} + </div>; + }; + } +})); window.ss = { i18n: { @@ -24,399 +50,365 @@ window.ss = { }, }; -const endpoints = { - verify: '/fake/{urlSegment}', -}; - -const mockRegisteredMethods = [ - { - urlSegment: 'aye', - name: 'aye', - component: 'Test', - }, - { - urlSegment: 'bee', - name: 'bee', - component: 'Test', - }, -]; - -const fetchMock = jest.spyOn(global, 'fetch'); - -describe('Verify', () => { - beforeEach(() => { - fetchMock.mockImplementation(() => Promise.resolve({ - status: 200, - json: () => Promise.resolve({}), - })); - fetchMock.mockClear(); - loadComponent.mockClear(); - }); - - it.skip('? if there are no registered methods', () => { - // Currently it renders nothing but there's undefined behaviour here - // TODO Update this test when there's defined behaviour - }); - - it('chooses a default method if none is given', () => { - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - /> - ); - - expect(wrapper.state('selectedMethod')).toEqual({ - urlSegment: 'aye', - name: 'aye', - component: 'Test', - }); - }); - - it('respects a given default method', () => { - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - defaultMethod="bee" - /> - ); - - expect(wrapper.state('selectedMethod')).toEqual({ - urlSegment: 'bee', - name: 'bee', - component: 'Test', - }); - }); - - it('fetches schema from the given login endpoint', () => { - shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - /> - ); - - expect(fetchMock.mock.calls).toHaveLength(1); - expect(fetchMock.mock.calls[0][0]).toEqual('/fake/aye'); +function makeProps(obj = {}) { + return { + endpoints: { + verify: '/fake/{urlSegment}', + }, + registeredMethods: [ + { + urlSegment: 'aye', + name: 'aye', + component: 'TestMethod', + }, + { + urlSegment: 'bee', + name: 'bee', + component: 'TestMethod', + }, + ], + SelectMethodComponenet: () => <div className="test-select-method"/>, + onCompleteVerification: () => null, + ...obj + }; +} + +test('Verify chooses a default method if none is given', async () => { + const { container } = render( + <Verify {...makeProps()}/> + ); + resolveApiCall({ + json: () => Promise.resolve({}), }); + await screen.findByText('Verify with {aye}'); + const titles = container.querySelectorAll('.mfa-section-title'); + expect(titles).toHaveLength(1); + expect(titles[0].textContent).toBe('Verify with {aye}'); +}); - it('loads the default method component', (done) => { - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - /> - ); - - expect(loadComponent.mock.calls).toHaveLength(1); - expect(loadComponent.mock.calls[0]).toEqual(['Test']); - - // Defer testing of final render state so that we don't inspect loading state - setTimeout(() => { - expect(wrapper.find('Test')).toHaveLength(1); - done(); - }); +test('Verify chooses a given default method', async () => { + const { container } = render( + <Verify {...makeProps({ + defaultMethod: 'bee' + })} + /> + ); + resolveApiCall({ + json: () => Promise.resolve({}), }); + await screen.findByText('Verify with {bee}'); + const titles = container.querySelectorAll('.mfa-section-title'); + expect(titles).toHaveLength(1); + expect(titles[0].textContent).toBe('Verify with {bee}'); +}); - it('forwards API response as props to injected component', (done) => { - fetchMock.mockImplementation(() => Promise.resolve({ - json: () => Promise.resolve({ - myProp: 1, - anotherProp: 'two', - }), - })); - - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - /> - ); - - // Defer testing of final render state so that we don't inspect loading state - setTimeout(() => { - expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ - myProp: 1, - anotherProp: 'two', - })); - done(); - }); +test('Verify fetches schema from the given login endpoint', async () => { + render( + <Verify {...makeProps()}/> + ); + resolveApiCall({ + json: () => Promise.resolve({}), }); - - it('handles a csrf token provided with the props and adds it to state', (done) => { - fetchMock.mockImplementation(() => Promise.resolve({ - json: () => Promise.resolve({ - SecurityID: 'test', - myProp: 1, - anotherProp: 'two', - }), - })); - - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - /> - ); - - // Defer testing of final render state so that we don't inspect loading state - setTimeout(() => { - expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ - myProp: 1, - anotherProp: 'two', - })); - expect(wrapper.state('token')).toBe('test'); - done(); - }); + expect(lastApiCallArgs).toStrictEqual({ + body: undefined, + endpoint: '/fake/aye', + headers: undefined, + method: undefined }); + // Do this final await to prevent "Warning: An update to Verify inside a test was not wrapped in act(...)" + await screen.findByText('Verify with {aye}'); +}); - it('provides a control to choose other methods to the injected component', (done) => { - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - /> - ); - - // Defer testing of final render state so that we don't inspect loading state - setTimeout(() => { - expect(shallow( - wrapper.find('Test').prop('moreOptionsControl') - ).matchesElement(<a>More options</a>)).toBe(true); - done(); - }); +test('Verify loads the default method component', async () => { + render( + <Verify {...makeProps()}/> + ); + resolveApiCall({ + json: () => Promise.resolve({}), }); + await screen.findByText('Verify with {aye}'); + expect(lastInjectorLoadComponentArg).toBe('TestMethod'); +}); - it('does not provides a control to choose other methods to the injected component when there are too few methods', (done) => { - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods.slice(0, 1)} - /> - ); - - // Defer testing of final render state so that we don't inspect loading state - setTimeout(() => { - expect(wrapper.find('Test').prop('moreOptionsControl')).toBeNull(); - done(); - }); +test('Verify forwards API response as props to injected component', async () => { + const { container } = render( + <Verify {...makeProps()}/> + ); + resolveApiCall({ + json: () => Promise.resolve({ + myProp: 1, + anotherProp: 'two', + }), }); + await screen.findByText('Verify with {aye}'); + const method = container.querySelector('[data-component="TestMethod"]'); + expect(method.getAttribute('data-props')).toContain('myProp=1,anotherProp=two'); +}); - it('handles a click event on the show other methods control', (done) => { - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - /> - ); - - // Defer testing of final render state so that we don't inspect loading state - setTimeout(() => { - const moreOptionsControl = shallow(wrapper.find('Test').prop('moreOptionsControl')); - const preventDefault = jest.fn(); - - moreOptionsControl.simulate('click', { preventDefault }); - - expect(preventDefault.mock.calls).toHaveLength(1); - expect(wrapper.state('showOtherMethods')).toBe(true); - expect(wrapper.find(SelectMethod)).toHaveLength(1); - done(); - }); +test('Verify handles a CSRF token provided with the props and adds it to state', async () => { + const { container } = render( + <Verify {...makeProps()}/> + ); + resolveApiCall({ + json: () => Promise.resolve({ + SecurityID: 'test', + }), }); - - it('will use the login endpoint to verify a completed login', (done) => { - const onCompleteVerification = jest.fn(); - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - onCompleteVerification={onCompleteVerification} - /> - ); - - setTimeout(() => { - expect(fetchMock.mock.calls).toHaveLength(1); - const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); - completionFunction({ test: 1 }); - expect(fetchMock.mock.calls).toHaveLength(2); - expect(fetchMock.mock.calls[1]).toEqual(['/fake/aye', { - credentials: 'same-origin', - method: 'POST', - headers: {}, - body: '{"test":1}', - }]); - setTimeout(() => { - expect(onCompleteVerification.mock.calls).toHaveLength(1); - done(); - }); - }); + await screen.findByText('Verify with {aye}'); + const method = container.querySelector('[data-component="TestMethod"]'); + fireEvent.click(method); + resolveApiCall({ + status: 200, + json: () => Promise.resolve({ foo: 'bar' }) }); - - it('will add a token from state when calling the verify endpoint', (done) => { - const onCompleteVerification = jest.fn(); - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - onCompleteVerification={onCompleteVerification} - /> - ); - - setTimeout(() => { - expect(fetchMock.mock.calls).toHaveLength(1); - wrapper.setState({ - token: 'test', - }); - const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); - completionFunction({ test: 1 }); - expect(fetchMock.mock.calls).toHaveLength(2); - expect(fetchMock.mock.calls[1][0]).toEqual('/fake/aye?SecurityID=test'); - done(); - }); + expect(lastApiCallArgs).toStrictEqual({ + body: '{"my":"verifydata"}', + endpoint: '/fake/aye?SecurityID=test', + headers: undefined, + method: 'POST' }); +}); - it('will provide a message from any unverified login to the injected component', done => { - const onCompleteVerification = jest.fn(); - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - onCompleteVerification={onCompleteVerification} - /> - ); - - setTimeout(() => { - expect(fetchMock.mock.calls).toHaveLength(1); - - fetchMock.mockImplementation(() => Promise.resolve({ - status: 400, - json: () => Promise.resolve({ - message: 'It was a failure', - }), - })); - fetchMock.mockClear(); - - const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); - completionFunction({ test: 1 }); - expect(fetchMock.mock.calls).toHaveLength(1); - expect(fetchMock.mock.calls[0]).toEqual(['/fake/aye', { - credentials: 'same-origin', - method: 'POST', - headers: {}, - body: '{"test":1}', - }]); - setTimeout(() => { - expect(onCompleteVerification.mock.calls).toHaveLength(0); - expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ - error: 'It was a failure', - })); - done(); - }); - }); +test('Verify provides a control to choose other methods to the injected component', async () => { + const { container } = render( + <Verify {...makeProps()}/> + ); + resolveApiCall({ + json: () => Promise.resolve({}), }); + await screen.findByText('Verify with {aye}'); + expect(container.querySelector('a.btn-secondary').textContent).toBe('More options'); +}); - it('will provide a try-again message for a 429 rate limiting code', done => { - const onCompleteVerification = jest.fn(); - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - onCompleteVerification={onCompleteVerification} - /> - ); - - setTimeout(() => { - expect(fetchMock.mock.calls).toHaveLength(1); - - fetchMock.mockImplementation(() => Promise.resolve({ - status: 429, - json: () => Promise.resolve({ - message: 'Something went wrong. Please try again.', - }), - })); - fetchMock.mockClear(); - - const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); - completionFunction({ test: 1 }); - expect(fetchMock.mock.calls).toHaveLength(1); - expect(fetchMock.mock.calls[0]).toEqual(['/fake/aye', { - credentials: 'same-origin', - method: 'POST', - headers: {}, - body: '{"test":1}', - }]); - setTimeout(() => { - expect(onCompleteVerification.mock.calls).toHaveLength(0); - expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ - error: 'Something went wrong. Please try again.', - })); - done(); - }); - }); +test('Verify does not provides a control to choose other methods to the injected component when there are too few methods', async () => { + const { container } = render( + <Verify {...makeProps({ + registeredMethods: [makeProps().registeredMethods[0]] + })} + /> + ); + resolveApiCall({ + json: () => Promise.resolve({}), }); + await screen.findByText('Verify with {aye}'); + expect(container.querySelector('a.btn-secondary')).toBeNull(); +}); - it('will provide an unknown message for a 500 rate limiting code', done => { - const onCompleteVerification = jest.fn(); - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - onCompleteVerification={onCompleteVerification} - /> - ); - - setTimeout(() => { - expect(fetchMock.mock.calls).toHaveLength(1); - - fetchMock.mockImplementation(() => Promise.resolve({ - status: 500, - json: () => Promise.resolve({ - message: 'An unknown error occurred.', - }), - })); - fetchMock.mockClear(); - - const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); - completionFunction({ test: 1 }); - expect(fetchMock.mock.calls).toHaveLength(1); - expect(fetchMock.mock.calls[0]).toEqual(['/fake/aye', { - credentials: 'same-origin', - method: 'POST', - headers: {}, - body: '{"test":1}', - }]); - setTimeout(() => { - expect(onCompleteVerification.mock.calls).toHaveLength(0); - expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ - error: 'An unknown error occurred.', - })); - done(); - }); - }); - }); - describe('renderSelectedMethod()', () => { - it('renders an unavailable screen when the selected method is unavailable', done => { - const wrapper = shallow( - <Verify - endpoints={endpoints} - registeredMethods={mockRegisteredMethods} - isAvailable={() => false} - getUnavailableMessage={() => 'There is no spoon'} - /> - ); - - // Defer testing of final render state so that we don't inspect loading state - setTimeout(() => { - // Enable a selected method - wrapper.instance().setState({ - selectedMethod: mockRegisteredMethods[0], - }); - - expect(wrapper.find(LoadingError)).toHaveLength(1); - done(); - }); - }); - }); -}); +// it('handles a click event on the show other methods control', (done) => { +// const wrapper = shallow( +// <Verify +// endpoints={endpoints} +// registeredMethods={mockRegisteredMethods} +// /> +// ); + +// // Defer testing of final render state so that we don't inspect loading state +// setTimeout(() => { +// const moreOptionsControl = shallow(wrapper.find('Test').prop('moreOptionsControl')); +// const preventDefault = jest.fn(); + +// moreOptionsControl.simulate('click', { preventDefault }); + +// expect(preventDefault.mock.calls).toHaveLength(1); +// expect(wrapper.state('showOtherMethods')).toBe(true); +// expect(wrapper.find(SelectMethod)).toHaveLength(1); +// done(); +// }); +// }); + +// it('will use the login endpoint to verify a completed login', (done) => { +// const onCompleteVerification = jest.fn(); +// const wrapper = shallow( +// <Verify +// endpoints={endpoints} +// registeredMethods={mockRegisteredMethods} +// onCompleteVerification={onCompleteVerification} +// /> +// ); + +// setTimeout(() => { +// expect(fetchMock.mock.calls).toHaveLength(1); +// const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); +// completionFunction({ test: 1 }); +// expect(fetchMock.mock.calls).toHaveLength(2); +// expect(fetchMock.mock.calls[1]).toEqual(['/fake/aye', { +// credentials: 'same-origin', +// method: 'POST', +// headers: {}, +// body: '{"test":1}', +// }]); +// setTimeout(() => { +// expect(onCompleteVerification.mock.calls).toHaveLength(1); +// done(); +// }); +// }); +// }); + +// it('will add a token from state when calling the verify endpoint', (done) => { +// const onCompleteVerification = jest.fn(); +// const wrapper = shallow( +// <Verify +// endpoints={endpoints} +// registeredMethods={mockRegisteredMethods} +// onCompleteVerification={onCompleteVerification} +// /> +// ); + +// setTimeout(() => { +// expect(fetchMock.mock.calls).toHaveLength(1); +// wrapper.setState({ +// token: 'test', +// }); +// const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); +// completionFunction({ test: 1 }); +// expect(fetchMock.mock.calls).toHaveLength(2); +// expect(fetchMock.mock.calls[1][0]).toEqual('/fake/aye?SecurityID=test'); +// done(); +// }); +// }); + +// it('will provide a message from any unverified login to the injected component', done => { +// const onCompleteVerification = jest.fn(); +// const wrapper = shallow( +// <Verify +// endpoints={endpoints} +// registeredMethods={mockRegisteredMethods} +// onCompleteVerification={onCompleteVerification} +// /> +// ); + +// setTimeout(() => { +// expect(fetchMock.mock.calls).toHaveLength(1); + +// fetchMock.mockImplementation(() => Promise.resolve({ +// status: 400, +// json: () => Promise.resolve({ +// message: 'It was a failure', +// }), +// })); +// fetchMock.mockClear(); + +// const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); +// completionFunction({ test: 1 }); +// expect(fetchMock.mock.calls).toHaveLength(1); +// expect(fetchMock.mock.calls[0]).toEqual(['/fake/aye', { +// credentials: 'same-origin', +// method: 'POST', +// headers: {}, +// body: '{"test":1}', +// }]); +// setTimeout(() => { +// expect(onCompleteVerification.mock.calls).toHaveLength(0); +// expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ +// error: 'It was a failure', +// })); +// done(); +// }); +// }); +// }); + +// it('will provide a try-again message for a 429 rate limiting code', done => { +// const onCompleteVerification = jest.fn(); +// const wrapper = shallow( +// <Verify +// endpoints={endpoints} +// registeredMethods={mockRegisteredMethods} +// onCompleteVerification={onCompleteVerification} +// /> +// ); + +// setTimeout(() => { +// expect(fetchMock.mock.calls).toHaveLength(1); + +// fetchMock.mockImplementation(() => Promise.resolve({ +// status: 429, +// json: () => Promise.resolve({ +// message: 'Something went wrong. Please try again.', +// }), +// })); +// fetchMock.mockClear(); + +// const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); +// completionFunction({ test: 1 }); +// expect(fetchMock.mock.calls).toHaveLength(1); +// expect(fetchMock.mock.calls[0]).toEqual(['/fake/aye', { +// credentials: 'same-origin', +// method: 'POST', +// headers: {}, +// body: '{"test":1}', +// }]); +// setTimeout(() => { +// expect(onCompleteVerification.mock.calls).toHaveLength(0); +// expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ +// error: 'Something went wrong. Please try again.', +// })); +// done(); +// }); +// }); +// }); + +// it('will provide an unknown message for a 500 rate limiting code', done => { +// const onCompleteVerification = jest.fn(); +// const wrapper = shallow( +// <Verify +// endpoints={endpoints} +// registeredMethods={mockRegisteredMethods} +// onCompleteVerification={onCompleteVerification} +// /> +// ); + +// setTimeout(() => { +// expect(fetchMock.mock.calls).toHaveLength(1); + +// fetchMock.mockImplementation(() => Promise.resolve({ +// status: 500, +// json: () => Promise.resolve({ +// message: 'An unknown error occurred.', +// }), +// })); +// fetchMock.mockClear(); + +// const completionFunction = wrapper.find('Test').prop('onCompleteVerification'); +// completionFunction({ test: 1 }); +// expect(fetchMock.mock.calls).toHaveLength(1); +// expect(fetchMock.mock.calls[0]).toEqual(['/fake/aye', { +// credentials: 'same-origin', +// method: 'POST', +// headers: {}, +// body: '{"test":1}', +// }]); +// setTimeout(() => { +// expect(onCompleteVerification.mock.calls).toHaveLength(0); +// expect(wrapper.find('Test').props()).toEqual(expect.objectContaining({ +// error: 'An unknown error occurred.', +// })); +// done(); +// }); +// }); +// }); + +// describe('renderSelectedMethod()', () => { +// it('renders an unavailable screen when the selected method is unavailable', done => { +// const wrapper = shallow( +// <Verify +// endpoints={endpoints} +// registeredMethods={mockRegisteredMethods} +// isAvailable={() => false} +// getUnavailableMessage={() => 'There is no spoon'} +// /> +// ); + +// // Defer testing of final render state so that we don't inspect loading state +// setTimeout(() => { +// // Enable a selected method +// wrapper.instance().setState({ +// selectedMethod: mockRegisteredMethods[0], +// }); + +// expect(wrapper.find(LoadingError)).toHaveLength(1); +// done(); +// }); +// }); +// }); +// }); diff --git a/client/src/containers/tests/Login-test.js b/client/src/containers/tests/Login-test.js index 60308f89..0ca3dd09 100644 --- a/client/src/containers/tests/Login-test.js +++ b/client/src/containers/tests/Login-test.js @@ -1,80 +1,83 @@ -/* global jest, describe, it, expect */ +/* global jest, test, expect */ -jest.mock('lib/Injector'); - -// eslint-disable-next-line no-unused-vars -import fetch from 'isomorphic-fetch'; import React from 'react'; -import Enzyme, { shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import LoadingError from 'components/LoadingError'; import { Component as Login } from '../Login'; +import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; + +let lastApiCallArgs; +let resolveApiCall; +let rejectApiCall; -Enzyme.configure({ adapter: new Adapter() }); +jest.mock('lib/api', () => ({ + __esModule: true, + default: (endpoint, method, body, headers) => { + lastApiCallArgs = { endpoint, method, body, headers }; + return new Promise((resolve, reject) => { + resolveApiCall = resolve; + rejectApiCall = reject; + }); + } +})); window.ss = { i18n: { _t: (key, string) => string }, }; -const fetchMock = jest.spyOn(global, 'fetch'); +const firstMethod = { + urlSegment: 'aye', + name: 'Aye', + description: 'Register using aye', + supportLink: 'https://google.com', + component: 'TestRegistration', +}; +const secondMethod = { + urlSegment: 'bee', + name: 'Bee', + description: 'Register using bee', + supportLink: '', + component: 'TestRegistration', +}; -describe('Login', () => { - beforeEach(() => { - fetchMock.mockClear(); +test('Login componentDidMount() handles schema fetch errors', async () => { + // Note you can't trigger an error by returning a 200 with an empty schema here as you'll + // get multiple other javascript errors instead of the "graceful" error state is triggered + render( + <Login {...{ + schemaURL: '/foo', + }} + /> + ); + resolveApiCall({ + status: 500 }); + expect(await screen.findByText('Something went wrong!')).not.toBeNull(); +}); - describe('componentDidMount()', () => { - it('handles schema fetch errors', done => { - fetchMock.mockImplementation(() => Promise.resolve({ - status: 500, - })); - - const wrapper = shallow( - <Login schemaURL="/foo" /> - ); - - setTimeout(() => { - expect(wrapper.instance().state.schemaLoaded).toBe(true); - done(); - }); - }); - - it('handles successful schema fetch', done => { - fetchMock.mockImplementation(() => Promise.resolve({ - status: 200, - json: () => Promise.resolve({ - schemaData: { allMethods: [] }, - }), - })); - - const wrapper = shallow( - <Login schemaURL="/foo" /> - ); - - setTimeout(() => { - expect(wrapper.instance().state.schema).toEqual({ - schemaData: { allMethods: [] }, - }); - done(); - }); - }); +test('Login componentDidMount() handles successful schema fetch', async () => { + let doResolve; + const promise = new Promise((resolve) => { + doResolve = resolve; }); - - describe('render()', () => { - it('renders an error screen', done => { - const wrapper = shallow( - <Login schemaURL="/foo" />, - { disableLifecycleMethods: true } - ); - - wrapper.instance().setState({ - loading: true, - schema: null, - schemaLoaded: true, - }, () => { - expect(wrapper.find(LoadingError)).toHaveLength(1); - done(); - }); - }); + const onSetAvailableMethods = jest.fn(() => doResolve()); + const { container } = render( + <Login {...{ + schemaURL: '/foo', + onSetAvailableMethods + }} + /> + ); + const indicator = container.querySelector('.mfa-loading-indicator'); + resolveApiCall({ + status: 200, + json: () => Promise.resolve({ + availableMethods: [firstMethod, secondMethod], + allMethods: [], + backupMethod: [], + registeredMethods: [], + isFullyRegistered: true, + }), }); + await waitForElementToBeRemoved(indicator); + await promise; + expect(onSetAvailableMethods).toBeCalled(); }); diff --git a/package.json b/package.json index 8e50b6e1..42cd0cbe 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@silverstripe/react-injector": "^0.2.1", + "@testing-library/react": "^14.0.0", "classnames": "^2.3.2", "core-js": "^3.26.0", "es6-promise": "^4.2.8", @@ -42,12 +43,8 @@ "@silverstripe/webpack-config": "^2.0.0", "babel-jest": "^29.2.2", "copy-webpack-plugin": "^11.0.0", - "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.6", "jest-cli": "^29.2.2", "jest-environment-jsdom": "^29.3.1", - "react-16": "npm:react@^16.14.0", - "react-dom-16": "npm:react-dom@^16.14.0", "webpack": "^5.74.0", "webpack-cli": "^5.0.0" }, @@ -59,11 +56,6 @@ ], "jest": { "testEnvironment": "jsdom", - "moduleNameMapper": { - "^react-dom/client$": "react-dom-16", - "^react-dom((/.*)?)$": "react-dom-16$1", - "^react((/.*)?)$": "react-16$1" - }, "roots": [ "client/src" ], diff --git a/yarn.lock b/yarn.lock index 91751a27..408f6f60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,13 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.10.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" + integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== + dependencies: + "@babel/highlight" "^7.18.6" + "@babel/compat-data@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" @@ -1602,11 +1609,39 @@ dependencies: "@sinonjs/commons" "^2.0.0" +"@testing-library/dom@^9.0.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.2.0.tgz#0e1f45e956f2a16f471559c06edd8827c4832f04" + integrity sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/react@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" + integrity sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^9.0.0" + "@types/react-dom" "^18.0.0" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@types/aria-query@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc" + integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q== + "@types/babel__core@^7.1.14": version "7.20.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" @@ -1739,6 +1774,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/react-dom@^18.0.0": + version "18.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" + integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== + dependencies: + "@types/react" "*" + "@types/react@*": version "17.0.43" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" @@ -2013,21 +2055,6 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -airbnb-prop-types@^2.16.0: - version "2.16.0" - resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2" - integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg== - dependencies: - array.prototype.find "^2.1.1" - function.prototype.name "^1.1.2" - is-regex "^1.1.0" - object-is "^1.1.2" - object.assign "^4.1.0" - object.entries "^1.1.2" - prop-types "^15.7.2" - prop-types-exact "^1.2.0" - react-is "^16.13.1" - ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2177,7 +2204,7 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^5.1.3: +aria-query@^5.0.0, aria-query@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== @@ -2206,35 +2233,6 @@ array-includes@^3.1.5, array-includes@^3.1.6: get-intrinsic "^1.1.3" is-string "^1.0.7" -array.prototype.filter@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.1.tgz#20688792acdb97a09488eaaee9eebbf3966aae21" - integrity sha512-Dk3Ty7N42Odk7PjU/Ci3zT4pLj20YvuVnneG/58ICM6bt4Ij5kZaJTVQ9TSaWaIECX2sFyz4KItkVZqHNnciqw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - -array.prototype.find@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.2.tgz#6abbd0c2573925d8094f7d23112306af8c16d534" - integrity sha512-00S1O4ewO95OmmJW7EesWfQlrCrLEL8kZ40w3+GkLX2yTt0m2ggcePPa2uHPJ9KUmJvwRq+lCV9bD8Yim23x/Q== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - -array.prototype.flat@^1.2.3: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" - integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - array.prototype.flat@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" @@ -2429,11 +2427,6 @@ binary-extensions@^2.0.0, binary-extensions@^2.2.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2582,30 +2575,6 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -cheerio-select@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" - integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== - dependencies: - css-select "^4.3.0" - css-what "^6.0.1" - domelementtype "^2.2.0" - domhandler "^4.3.1" - domutils "^2.8.0" - -cheerio@^1.0.0-rc.3: - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" - integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== - dependencies: - cheerio-select "^1.5.0" - dom-serializer "^1.3.2" - domhandler "^4.2.0" - htmlparser2 "^6.1.0" - parse5 "^6.0.1" - parse5-htmlparser2-tree-adapter "^6.0.1" - tslib "^2.2.0" - "chokidar@>=3.0.0 <4.0.0": version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -2805,7 +2774,7 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^2.19.0, commander@^2.20.0, commander@^2.8.1: +commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -2940,22 +2909,6 @@ css-loader@^6.7.1: postcss-value-parser "^4.2.0" semver "^7.3.8" -css-select@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-what@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -3158,11 +3111,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -discontinuous-range@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" - integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= - doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -3185,6 +3133,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-helpers@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" @@ -3192,20 +3145,6 @@ dom-helpers@^3.4.0: dependencies: "@babel/runtime" "^7.1.2" -dom-serializer@^1.0.1, dom-serializer@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== - domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -3213,22 +3152,6 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domutils@^2.5.2, domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -3274,11 +3197,6 @@ enhanced-resolve@^5.10.0: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - entities@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" @@ -3299,78 +3217,6 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== -enzyme-adapter-react-16@^1.15.6: - version "1.15.7" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.7.tgz#a737e6d8e2c147e9da5acf957755be7634f76201" - integrity sha512-LtjKgvlTc/H7adyQcj+aq0P0H07LDL480WQl1gU512IUyaDo/sbOaNDdZsJXYW2XaoPqrLLE9KbZS+X2z6BASw== - dependencies: - enzyme-adapter-utils "^1.14.1" - enzyme-shallow-equal "^1.0.5" - has "^1.0.3" - object.assign "^4.1.4" - object.values "^1.1.5" - prop-types "^15.8.1" - react-is "^16.13.1" - react-test-renderer "^16.0.0-0" - semver "^5.7.0" - -enzyme-adapter-utils@^1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz#f30db15dafc22e0ccd44f5acc8d93be29218cdcf" - integrity sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ== - dependencies: - airbnb-prop-types "^2.16.0" - function.prototype.name "^1.1.5" - has "^1.0.3" - object.assign "^4.1.4" - object.fromentries "^2.0.5" - prop-types "^15.8.1" - semver "^5.7.1" - -enzyme-shallow-equal@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e" - integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q== - dependencies: - has "^1.0.3" - object-is "^1.1.2" - -enzyme-shallow-equal@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz#5528a897a6ad2bdc417c7221a7db682cd01711ba" - integrity sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg== - dependencies: - has "^1.0.3" - object-is "^1.1.5" - -enzyme@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28" - integrity sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw== - dependencies: - array.prototype.flat "^1.2.3" - cheerio "^1.0.0-rc.3" - enzyme-shallow-equal "^1.0.1" - function.prototype.name "^1.1.2" - has "^1.0.3" - html-element-map "^1.2.0" - is-boolean-object "^1.0.1" - is-callable "^1.1.5" - is-number-object "^1.0.4" - is-regex "^1.0.5" - is-string "^1.0.5" - is-subset "^0.1.1" - lodash.escape "^4.0.1" - lodash.isequal "^4.5.0" - object-inspect "^1.7.0" - object-is "^1.0.2" - object.assign "^4.1.0" - object.entries "^1.1.1" - object.values "^1.1.1" - raf "^3.4.1" - rst-selector-parser "^2.2.3" - string.prototype.trim "^1.2.1" - err-code@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" @@ -3448,11 +3294,6 @@ es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - es-get-iterator@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" @@ -4138,7 +3979,7 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.1.2, function.prototype.name@^1.1.5: +function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== @@ -4443,14 +4284,6 @@ hosted-git-info@^5.0.0, hosted-git-info@^5.2.1: dependencies: lru-cache "^7.5.1" -html-element-map@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.3.1.tgz#44b2cbcfa7be7aa4ff59779e47e51012e1c73c08" - integrity sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg== - dependencies: - array.prototype.filter "^1.0.0" - call-bind "^1.0.2" - html-encoding-sniffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" @@ -4463,16 +4296,6 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlparser2@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -4709,7 +4532,7 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: +is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== @@ -4717,7 +4540,7 @@ is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.4: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== @@ -4854,7 +4677,7 @@ is-property@^1.0.0, is-property@^1.0.2: resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= -is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: +is-regex@^1.0.4, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -4896,11 +4719,6 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" -is-subset@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" - integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= - is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -5783,21 +5601,6 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== -lodash.escape@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" - integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= - -lodash.flattendeep@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" - integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= - lodash.kebabcase@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -5839,6 +5642,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -6084,11 +5892,6 @@ moment@^2.29.4: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== -moo@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" - integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== - mrmime@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" @@ -6129,16 +5932,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -nearley@^2.7.10: - version "2.20.1" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" - integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== - dependencies: - commander "^2.19.0" - moo "^0.5.0" - railroad-diagrams "^1.0.0" - randexp "0.4.6" - negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -6404,13 +6197,6 @@ npmlog@^6.0.0, npmlog@^6.0.2: gauge "^4.0.3" set-blocking "^2.0.0" -nth-check@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" - integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== - dependencies: - boolbase "^1.0.0" - number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -6426,7 +6212,7 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-inspect@^1.12.0, object-inspect@^1.7.0, object-inspect@^1.9.0: +object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== @@ -6436,7 +6222,7 @@ object-inspect@^1.12.2: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== -object-is@^1.0.1, object-is@^1.0.2, object-is@^1.1.2, object-is@^1.1.5: +object-is@^1.0.1, object-is@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== @@ -6449,7 +6235,7 @@ object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -6469,7 +6255,7 @@ object.assign@^4.1.3, object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.1, object.entries@^1.1.2, object.entries@^1.1.5: +object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== @@ -6487,15 +6273,6 @@ object.entries@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" -object.fromentries@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" - integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - object.fromentries@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" @@ -6513,15 +6290,6 @@ object.hasown@^1.1.2: define-properties "^1.1.4" es-abstract "^1.20.4" -object.values@^1.1.1, object.values@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - object.values@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" @@ -6677,18 +6445,6 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5-htmlparser2-tree-adapter@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" - integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== - dependencies: - parse5 "^6.0.1" - -parse5@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - parse5@^7.0.0, parse5@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -6726,11 +6482,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -6847,6 +6598,15 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-format@^29.4.0: version "29.4.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.0.tgz#766f071bb1c53f1ef8000c105bbeb649e86eb993" @@ -6914,15 +6674,6 @@ promzard@^0.3.0: dependencies: read "1" -prop-types-exact@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" - integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== - dependencies: - has "^1.0.3" - object.assign "^4.1.0" - reflect.ownkeys "^0.2.0" - prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -6957,26 +6708,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -raf@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - -railroad-diagrams@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" - integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= - -randexp@0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" - integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== - dependencies: - discontinuous-range "1.0.0" - ret "~0.1.10" - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -6984,15 +6715,6 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -"react-16@npm:react@^16.14.0", react@^16.8.3: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - react-copy-to-clipboard@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c" @@ -7001,7 +6723,7 @@ react-copy-to-clipboard@^5.1.0: copy-to-clipboard "^3.3.1" prop-types "^15.8.1" -"react-dom-16@npm:react-dom@^16.14.0", react-dom@^16.8.3: +react-dom@^16.8.3: version "16.14.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== @@ -7019,11 +6741,16 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" -react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.6: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" @@ -7059,16 +6786,6 @@ react-redux@^8.0.4: react-is "^18.0.0" use-sync-external-store "^1.0.0" -react-test-renderer@^16.0.0-0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae" - integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg== - dependencies: - object-assign "^4.1.1" - prop-types "^15.6.2" - react-is "^16.8.6" - scheduler "^0.19.1" - react-transition-group@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-3.0.0.tgz#36efa4db970d5eec5e3028e0c458931163fa3b9b" @@ -7079,6 +6796,15 @@ react-transition-group@^3.0.0: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" +react@^16.8.3: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -7196,11 +6922,6 @@ redux@^4.2.0: dependencies: "@babel/runtime" "^7.9.2" -reflect.ownkeys@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" - integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= - regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" @@ -7387,11 +7108,6 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -7416,14 +7132,6 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" -rst-selector-parser@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91" - integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE= - dependencies: - lodash.flattendeep "^4.4.0" - nearley "^2.7.10" - run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -7545,11 +7253,6 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" -semver@^5.7.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -7789,15 +7492,6 @@ string.prototype.matchall@^4.0.8: regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" -string.prototype.trim@^1.2.1: - version "1.2.5" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.5.tgz#a587bcc8bfad8cb9829a577f5de30dd170c1682c" - integrity sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - string.prototype.trimend@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" @@ -8076,11 +7770,6 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"