From 3bc47194ef7c5d7c9d68a48494b05f6528205a17 Mon Sep 17 00:00:00 2001 From: Bruno Dias Date: Sat, 10 Jun 2017 12:54:33 -0300 Subject: [PATCH] [chore] refactoring tests... * separate tests for events, styles and state. * added helpers to find dom nodes, dispatch events... * simplify assertitions. --- specs/Modal.events.spec.js | 137 +++++++++++ specs/Modal.spec.js | 471 +++++++++++-------------------------- specs/Modal.style.spec.js | 65 +++++ specs/helper.js | 101 +++++++- 4 files changed, 443 insertions(+), 331 deletions(-) create mode 100644 specs/Modal.events.spec.js create mode 100644 specs/Modal.style.spec.js diff --git a/specs/Modal.events.spec.js b/specs/Modal.events.spec.js new file mode 100644 index 00000000..bca42ed4 --- /dev/null +++ b/specs/Modal.events.spec.js @@ -0,0 +1,137 @@ +import sinon from 'sinon'; +import expect from 'expect'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; +import Modal from '../lib/components/Modal'; +import * as ariaAppHider from '../lib/helpers/ariaAppHider'; +import { + isBodyWithReactModalOpenClass, findDOMWithClass, + contentAttribute, overlayAttribute, + moverlay, mcontent, + clickAt, mouseDownAt, mouseUpAt, escKeyDown, tabKeyDown, + renderModal, unmountModal, emptyDOM +} from './helper'; + +describe('Events', () => { + afterEach('Unmount modal', emptyDOM); + + it('should trigger the onAfterOpen callback', () => { + var afterOpenCallback = sinon.spy(); + renderModal({ isOpen: true, onAfterOpen: afterOpenCallback }); + expect(afterOpenCallback.called).toBeTruthy(); + }); + + it('keeps focus inside the modal when child has no tabbable elements', () => { + var tabPrevented = false; + var modal = renderModal({ isOpen: true }, 'hello'); + const content = mcontent(modal); + expect(document.activeElement).toEqual(content); + tabKeyDown(content, { + preventDefault: function() { tabPrevented = true; } + }); + expect(tabPrevented).toEqual(true); + }); + + it('handles case when child has no tabbable elements', () => { + var modal = renderModal({ isOpen: true }, 'hello'); + const content = mcontent(modal); + tabKeyDown(content); + expect(document.activeElement).toEqual(content); + }); + + it('should close on Esc key event', () => { + var requestCloseCallback = sinon.spy(); + var modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: requestCloseCallback + }); + escKeyDown(mcontent(modal)); + expect(requestCloseCallback.called).toBeTruthy(); + // Check if event is passed to onRequestClose callback. + var event = requestCloseCallback.getCall(0).args[0]; + expect(event).toExist(); + }); + + it('verify overlay click when shouldCloseOnOverlayClick sets to false', () => { + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: false + }); + var overlay = moverlay(modal); + clickAt(overlay); + expect(!requestCloseCallback.called).toBeTruthy(); + }); + + it('verify overlay click when shouldCloseOnOverlayClick sets to true', () => { + var requestCloseCallback = sinon.spy(); + var modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: function() { + requestCloseCallback(); + } + }); + clickAt(moverlay(modal)); + expect(requestCloseCallback.called).toBeTruthy(); + }); + + it('verify overlay mouse down and content mouse up when shouldCloseOnOverlayClick sets to true', () => { + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: requestCloseCallback + }); + mouseDownAt(moverlay(modal)); + mouseUpAt(mcontent(modal)); + expect(!requestCloseCallback.called).toBeTruthy(); + }); + + it('verify content mouse down and overlay mouse up when shouldCloseOnOverlayClick sets to true', () => { + var requestCloseCallback = sinon.spy(); + var modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: function() { + requestCloseCallback(); + } + }); + mouseDownAt(mcontent(modal)); + mouseUpAt(moverlay(modal)); + expect(!requestCloseCallback.called).toBeTruthy(); + }); + + it('should not stop event propagation', () => { + var hasPropagated = false; + var modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true + }); + window.addEventListener('click', () => { + hasPropagated = true; + }); + moverlay(modal).dispatchEvent(new MouseEvent('click', { bubbles: true })); + expect(hasPropagated).toBeTruthy(); + }); + + it('verify event passing on overlay click', () => { + var requestCloseCallback = sinon.spy(); + var modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: requestCloseCallback + }); + // click the overlay + clickAt(moverlay(modal), { + // Used to test that this was the event received + fakeData: 'ABC' + }); + expect(requestCloseCallback.called).toBeTruthy(); + // Check if event is passed to onRequestClose callback. + var event = requestCloseCallback.getCall(0).args[0]; + expect(event).toExist(); + }); +}); diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index 65e80c22..97d85494 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -1,32 +1,56 @@ /* eslint-env mocha */ -import TestUtils from 'react-addons-test-utils'; +import sinon from 'sinon'; +import expect from 'expect'; import React from 'react'; import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; import Modal from '../lib/components/Modal'; import * as ariaAppHider from '../lib/helpers/ariaAppHider'; -import { renderModal, unmountModal, emptyDOM } from './helper'; +import { + isBodyWithReactModalOpenClass, findDOMWithClass, + contentAttribute, overlayAttribute, + mcontent, moverlay, + clickAt, mouseDownAt, mouseUpAt, escKeyDown, tabKeyDown, + renderModal, unmountModal, emptyDOM +} from './helper'; + +import * as events from './Modal.events.spec'; +import * as styles from './Modal.style.spec'; const Simulate = TestUtils.Simulate; -import sinon from 'sinon'; -import expect from 'expect'; -describe('Modal', () => { +describe('State', () => { afterEach('check if test cleaned up rendered modals', emptyDOM); it('scopes tab navigation to the modal'); it('focuses the last focused element when tabbing in from browser chrome'); + it('renders children [tested indirectly]'); it('can be open initially', () => { - const component = renderModal({ isOpen: true }, 'hello'); - expect(component.portal.refs.content.innerHTML.trim()).toEqual('hello'); + const modal = renderModal({ isOpen: true }, 'hello'); + expect(mcontent(modal)).toExist(); }); - it('can be closed initially', function() { - var component = renderModal({}, 'hello'); - expect(ReactDOM.findDOMNode(component.portal).innerHTML.trim()).toEqual(''); + it('can be closed initially', () => { + var modal = renderModal({}, 'hello'); + expect(ReactDOM.findDOMNode(mcontent(modal))).toNotExist(); }); - it('accepts appElement as a prop', function() { + it('has default props', () => { + var node = document.createElement('div'); + Modal.setAppElement(document.createElement('div')); + var modal = ReactDOM.render(, node); + var props = modal.props; + expect(props.isOpen).toBe(false); + expect(props.ariaHideApp).toBe(true); + expect(props.closeTimeoutMS).toBe(0); + expect(props.shouldCloseOnOverlayClick).toBe(true); + ReactDOM.unmountComponentAtNode(node); + ariaAppHider.resetForTesting(); + Modal.setAppElement(document.body); // restore default + }); + + it('accepts appElement as a prop', () => { var el = document.createElement('div'); var node = document.createElement('div'); ReactDOM.render(( @@ -36,247 +60,179 @@ describe('Modal', () => { ReactDOM.unmountComponentAtNode(node); }); - it('renders into the body, not in context', function() { + it('renders into the body, not in context', () => { var node = document.createElement('div'); var App = React.createClass({ render() { - return ( -
- - hello - -
- ); + return ( +
+ + hello + +
+ ); } }); Modal.setAppElement(node); ReactDOM.render(, node); - var modalParent = document.body.querySelector('.ReactModalPortal').parentNode; - expect(modalParent).toEqual(document.body); + expect( + document.body.querySelector('.ReactModalPortal').parentNode + ).toEqual( + document.body + ); ReactDOM.unmountComponentAtNode(node); }); - it('renders children [tested indirectly]'); - - it('renders the modal content with a dialog aria role when provided ', function () { + it('renders the modal content with a dialog aria role when provided ', () => { var child = 'I am a child of Modal, and he has sent me here...'; - var component = renderModal({isOpen: true, role: 'dialog'}, child); - expect(component.portal.refs.content.getAttribute('role')).toEqual('dialog'); + var modal = renderModal({ isOpen: true, role: 'dialog' }, child); + expect(contentAttribute(modal, 'role')).toEqual('dialog'); }); - it('renders the modal with a aria-label based on the contentLabel prop', function () { + it('renders the modal with a aria-label based on the contentLabel prop', () => { var child = 'I am a child of Modal, and he has sent me here...'; - var component = renderModal({isOpen: true, contentLabel: 'Special Modal'}, child); - expect(component.portal.refs.content.getAttribute('aria-label')).toEqual('Special Modal'); - }); - - it('has default props', function() { - var node = document.createElement('div'); - Modal.setAppElement(document.createElement('div')); - var component = ReactDOM.render(, node); - var props = component.props; - expect(props.isOpen).toBe(false); - expect(props.ariaHideApp).toBe(true); - expect(props.closeTimeoutMS).toBe(0); - expect(props.shouldCloseOnOverlayClick).toBe(true); - ReactDOM.unmountComponentAtNode(node); - ariaAppHider.resetForTesting(); - Modal.setAppElement(document.body); // restore default + var modal = renderModal({ isOpen: true, contentLabel: 'Special Modal' }, child); + expect(contentAttribute(modal, 'aria-label')).toEqual('Special Modal'); }); - it('removes the portal node', function() { - var component = renderModal({isOpen: true}, 'hello'); - expect(component.portal.refs.content.innerHTML.trim()).toEqual('hello'); + it('removes the portal node', () => { + var modal = renderModal({ isOpen: true }, 'hello'); unmountModal(); - expect(!document.querySelector('.ReactModalPortal')).toExist(); + expect(document.querySelector('.ReactModalPortal')).toNotExist(); }); - it('focuses the modal content', function(done) { - renderModal({isOpen: true}, null, function () { - expect(document.activeElement).toEqual(this.portal.refs.content); - done(); - }); + it('focuses the modal content', () => { + const modal = renderModal({ isOpen: true }, null); + expect(document.activeElement).toBe(mcontent(modal)); }); - it('give back focus to previous element or modal.', (done) => { + it('give back focus to previous element or modal.', done => { function cleanup () { unmountModal(); done(); } - const modal = renderModal({ + const modalA = renderModal({ isOpen: true, + className: 'modal-a', onRequestClose () { - cleanup(); + cleanup(); } }, null); - renderModal({ + const modalContent = mcontent(modalA); + expect(document.activeElement).toEqual(modalContent); + + const modalB = renderModal({ isOpen: true, - onRequestClose: function () { - Simulate.keyDown(modal.portal.refs.content, { - // The keyCode is all that matters, so this works - key: 'esc', - keyCode: 27, - which: 27 - }); - expect(document.activeElement).toEqual(modal.portal.refs.content); + className: 'modal-b', + onRequestClose: function() { + const modalContent = mcontent(modalB); + expect(document.activeElement).toEqual(mcontent(modalA)); + escKeyDown(modalContent); + expect(document.activeElement).toEqual(modalContent); } - }, null, function checkPortalFocus () { - expect(document.activeElement).toEqual(this.portal.refs.content); - Simulate.keyDown(this.portal.refs.content, { - // The keyCode is all that matters, so this works - key: 'esc', - keyCode: 27, - which: 27 - }); - }); + }, null); + + escKeyDown(modalContent); }); - it('does not focus the modal content when a descendent is already focused', function() { + it('does not steel focus when a descendent is already focused', () => { + var content; var input = ( - { el && el.focus(); }} /> + { el && el.focus(); content = el; }} /> ); - - renderModal({isOpen: true}, input, function () { - expect(document.activeElement).toEqual(document.querySelector('.focus_input')); + renderModal({ isOpen: true }, input, function () { + expect(document.activeElement).toEqual(content); }); }); - it('handles case when child has no tabbable elements', function() { - var component = renderModal({isOpen: true}, 'hello'); - Simulate.keyDown(component.portal.refs.content, {key: "Tab", keyCode: 9, which: 9}); - }); - - it('keeps focus inside the modal when child has no tabbable elements', function() { - var tabPrevented = false; - var modal = renderModal({isOpen: true}, 'hello'); - expect(document.activeElement).toEqual(modal.portal.refs.content); - Simulate.keyDown(modal.portal.refs.content, { - key: "Tab", - keyCode: 9, - which: 9, - preventDefault: function() { tabPrevented = true; } + it('supports portalClassName', () => { + var modal = renderModal({ + isOpen: true, + portalClassName: 'myPortalClass' }); - expect(tabPrevented).toEqual(true); - }); - - it('supports portalClassName', function () { - var modal = renderModal({isOpen: true, portalClassName: 'myPortalClass'}); - expect(modal.node.className).toEqual('myPortalClass'); + expect(modal.node.className.includes('myPortalClass')).toBeTruthy(); }); it('supports custom className', () => { const modal = renderModal({ isOpen: true, className: 'myClass' }); expect( - modal.portal.refs.content.className.indexOf('myClass') - ).toNotEqual(-1); + mcontent(modal).className.includes('myClass') + ).toBeTruthy(); }); it('supports overlayClassName', () => { - const modal = renderModal({ isOpen: true, overlayClassName: 'myOverlayClass' }); + const modal = renderModal({ + isOpen: true, + overlayClassName: 'myOverlayClass' + }); expect( - modal.portal.refs.overlay.className.indexOf('myOverlayClass') - ).toNotEqual(-1); - }); - - it('overrides the default styles when a custom classname is used', () => { - const modal = renderModal({ isOpen: true, className: 'myClass' }); - expect(modal.portal.refs.content.style.top).toEqual(''); - }); - - it('overrides the default styles when a custom overlayClassName is used', () => { - const modal = renderModal({ isOpen: true, overlayClassName: 'myOverlayClass' }); - expect(modal.portal.refs.overlay.style.backgroundColor).toEqual(''); - }); - - it('supports adding style to the modal contents', () => { - const modal = renderModal({ isOpen: true, style: { content: { width: '20px' } } }); - expect(modal.portal.refs.content.style.width).toEqual('20px'); + moverlay(modal).className.includes('myOverlayClass') + ).toBeTruthy(); }); - it('supports overriding style on the modal contents', () => { - const modal = renderModal({ isOpen: true, style: { content: { position: 'static' } } }); - expect(modal.portal.refs.content.style.position).toEqual('static'); - }); - - it('supports adding style on the modal overlay', () => { - const modal = renderModal({ isOpen: true, style: { overlay: { width: '75px' } } }); - expect(modal.portal.refs.overlay.style.width).toEqual('75px'); - }); - - it('supports overriding style on the modal overlay', () => { - const modal = renderModal({ isOpen: true, style: { overlay: { position: 'static' } } }); - expect(modal.portal.refs.overlay.style.position).toEqual('static'); - }); - - it('supports overriding the default styles', () => { - const previousStyle = Modal.defaultStyles.content.position; - // Just in case the default style is already relative, check that we can change it - const newStyle = previousStyle === 'relative' ? 'static' : 'relative'; - Modal.defaultStyles.content.position = newStyle; - const modal = renderModal({ isOpen: true }); - expect(modal.portal.refs.content.style.position).toEqual(newStyle); - Modal.defaultStyles.content.position = previousStyle; + it('don\'t append class to document.body if modal is not open', () => { + renderModal({ isOpen: false }); + expect(!isBodyWithReactModalOpenClass()).toBeTruthy(); + unmountModal(); }); - it('should remove class from body when no modals opened', () => { - const findReactModalOpenClass = () => document.body.className.indexOf('ReactModal__Body--open'); - renderModal({ isOpen: true }); + it('append class to document.body if modal is open', () => { renderModal({ isOpen: true }); - expect(findReactModalOpenClass() > -1).toBe(true); + expect(isBodyWithReactModalOpenClass()).toBeTruthy(); unmountModal(); - expect(findReactModalOpenClass() > -1).toBe(true); - unmountModal(); - expect(findReactModalOpenClass() === -1).toBe(true); }); - it('adds class to body when open', function() { - renderModal({ isOpen: false }); - expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(false); - unmountModal(); - + it('removes class from document.body when unmounted without closing', () => { renderModal({ isOpen: true }); - expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(true); unmountModal(); - - renderModal({ isOpen: false }); - expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(false); + expect(!isBodyWithReactModalOpenClass()).toBeTruthy(); }); - it('removes class from body when unmounted without closing', function() { - renderModal({isOpen: true}); - expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(true); + it('remove class from document.body when no modals opened', () => { + renderModal({ isOpen: true }); + renderModal({ isOpen: true }); + expect(isBodyWithReactModalOpenClass()).toBeTruthy(); + unmountModal(); + expect(isBodyWithReactModalOpenClass()).toBeTruthy(); unmountModal(); - expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(false); + expect(!isBodyWithReactModalOpenClass()).toBeTruthy(); }); - it('removes aria-hidden from appElement when unmounted without closing', function() { + it('removes aria-hidden from appElement when unmounted without closing', () => { var el = document.createElement('div'); var node = document.createElement('div'); - ReactDOM.render(React.createElement(Modal, { - isOpen: true, - appElement: el - }), node); + ReactDOM.render(( + + ), node); expect(el.getAttribute('aria-hidden')).toEqual('true'); ReactDOM.unmountComponentAtNode(node); expect(el.getAttribute('aria-hidden')).toEqual(null); }); - it('adds --after-open for animations', function() { - renderModal({isOpen: true}); - var overlay = document.querySelector('.ReactModal__Overlay'); - var content = document.querySelector('.ReactModal__Content'); - expect(overlay.className.match(/ReactModal__Overlay--after-open/)).toExist(); - expect(content.className.match(/ReactModal__Content--after-open/)).toExist(); + it('adds --after-open for animations', () => { + const modal = renderModal({ isOpen: true }); + var rg = /--after-open/i; + expect(rg.test(mcontent(modal).className)).toBeTruthy(); + expect(rg.test(moverlay(modal).className)).toBeTruthy(); }); - it('should trigger the onAfterOpen callback', function() { - var afterOpenCallback = sinon.spy(); - renderModal({ isOpen: true, onAfterOpen: afterOpenCallback }); - expect(afterOpenCallback.called).toBeTruthy(); + it('adds --before-close for animations', () => { + const closeTimeoutMS = 50; + const modal = renderModal({ + isOpen: true, + closeTimeoutMS + }); + modal.portal.closeWithTimeout(); + + const rg = /--before-close/i; + expect(rg.test(moverlay(modal).className)).toBeTruthy(); + expect(rg.test(mcontent(modal).className)).toBeTruthy(); + + modal.portal.closeWithoutTimeout(); }); - it('check the state of the modal after close with time out and reopen it', function() { + it('check the state of the modal after close with time out and reopen it', () => { var modal = renderModal({ isOpen: true, closeTimeoutMS: 2000, @@ -288,163 +244,18 @@ describe('Modal', () => { expect(!modal.portal.state.isOpen).toBeTruthy(); }); - describe('should close on overlay click', () => { - afterEach('Unmount modal', emptyDOM); - - describe('verify props', () => { - afterEach('Unmount modal', emptyDOM); - - it('verify default prop of shouldCloseOnOverlayClick', () => { - const modal = renderModal({ isOpen: true }); - expect(modal.props.shouldCloseOnOverlayClick).toEqual(true); - }); - - it('verify prop of shouldCloseOnOverlayClick', function () { - var modal = renderModal({isOpen: true, shouldCloseOnOverlayClick: false}); - expect(modal.props.shouldCloseOnOverlayClick).toEqual(false); - }); - }); - - describe('verify clicks', () => { - afterEach('Unmount modal', emptyDOM); - - it('verify overlay click when shouldCloseOnOverlayClick sets to false', () => { - const requestCloseCallback = sinon.spy(); - const modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: false - }); - expect(modal.props.isOpen).toEqual(true); - var overlay = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Overlay'); - expect(overlay.length).toEqual(1); - Simulate.click(overlay[0]); // click the overlay - expect(!requestCloseCallback.called).toBeTruthy(); - }); - - it('verify overlay click when shouldCloseOnOverlayClick sets to true', function() { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: function() { - requestCloseCallback(); - } - }); - expect(modal.props.isOpen).toEqual(true); - var overlay = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Overlay'); - expect(overlay.length).toEqual(1); - Simulate.click(overlay[0]); // click the overlay - expect(requestCloseCallback.called).toBeTruthy(); - }); - - it('verify overlay mouse down and content mouse up when shouldCloseOnOverlayClick sets to true', () => { - const requestCloseCallback = sinon.spy(); - const modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: requestCloseCallback - }); - expect(modal.props.isOpen).toEqual(true); - var overlay = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Overlay'); - var content = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Content'); - expect(overlay.length).toEqual(1); - expect(content.length).toEqual(1); - Simulate.mouseDown(overlay[0]); // click the overlay - Simulate.mouseUp(content[0]); - expect(!requestCloseCallback.called).toBeTruthy(); - }); - - it('verify content mouse down and overlay mouse up when shouldCloseOnOverlayClick sets to true', function() { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: function() { - requestCloseCallback(); - } - }); - expect(modal.props.isOpen).toEqual(true); - var overlay = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Overlay'); - var content = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Content'); - expect(content.length).toEqual(1); - expect(overlay.length).toEqual(1); - Simulate.mouseDown(content[0]); // click the overlay - Simulate.mouseUp(overlay[0]); - expect(!requestCloseCallback.called).toBeTruthy(); - }); - - it('should not stop event propagation', function() { - var hasPropagated = false - var modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true - }); - var overlay = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Overlay'); - window.addEventListener('click', function () { hasPropagated = true }) - overlay[0].dispatchEvent(new MouseEvent('click', { bubbles: true })) - expect(hasPropagated).toBeTruthy(); - }); - }); - - it('verify event passing on overlay click', function() { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: requestCloseCallback - }); - expect(modal.props.isOpen).toEqual(true); - var overlay = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Overlay'); - expect(overlay.length).toEqual(1); - // click the overlay - Simulate.click(overlay[0], { - // Used to test that this was the event received - fakeData: 'ABC' - }); - expect(requestCloseCallback.called).toBeTruthy(); - // Check if event is passed to onRequestClose callback. - var event = requestCloseCallback.getCall(0).args[0]; - expect(event).toBeTruthy(); - expect(event.constructor).toBeTruthy(); - expect(event.constructor.name).toEqual('SyntheticEvent'); - }); - }); - - it('should close on Esc key event', function() { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: requestCloseCallback - }); - expect(modal.props.isOpen).toEqual(true); - Simulate.keyDown(modal.portal.refs.content, {key: "Esc", keyCode: 27, which: 27}) - expect(requestCloseCallback.called).toBeTruthy(); - // Check if event is passed to onRequestClose callback. - var event = requestCloseCallback.getCall(0).args[0]; - expect(event).toBeTruthy(); - expect(event.constructor).toBeTruthy(); - expect(event.constructor.name).toEqual('SyntheticEvent'); + it('verify default prop of shouldCloseOnOverlayClick', () => { + const modal = renderModal({ isOpen: true }); + expect(modal.props.shouldCloseOnOverlayClick).toBeTruthy(); }); - it('adds --before-close for animations', () => { - const closeTimeoutMS = 50; - const modal = renderModal({ - isOpen: true, - closeTimeoutMS - }); - modal.portal.closeWithTimeout(); - - const overlay = TestUtils.findRenderedDOMComponentWithClass(modal.portal, 'ReactModal__Overlay'); - const content = TestUtils.findRenderedDOMComponentWithClass(modal.portal, 'ReactModal__Content'); - - expect(/ReactModal__Overlay--before-close/.test(overlay.className)).toBe(true); - expect(/ReactModal__Content--before-close/.test(content.className)).toBe(true); - - modal.portal.closeWithoutTimeout(); + it('verify prop of shouldCloseOnOverlayClick', () => { + var modalOpts = { isOpen: true, shouldCloseOnOverlayClick: false }; + var modal = renderModal(modalOpts); + expect(!modal.props.shouldCloseOnOverlayClick).toBeTruthy(); }); - it('keeps the modal in the DOM until closeTimeoutMS elapses', (done) => { + it('keeps the modal in the DOM until closeTimeoutMS elapses', done => { const closeTimeoutMS = 100; const modal = renderModal({ isOpen: true, closeTimeoutMS }); diff --git a/specs/Modal.style.spec.js b/specs/Modal.style.spec.js new file mode 100644 index 00000000..cb8a3b45 --- /dev/null +++ b/specs/Modal.style.spec.js @@ -0,0 +1,65 @@ +import sinon from 'sinon'; +import expect from 'expect'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react-addons-test-utils'; +import Modal from '../lib/components/Modal'; +import * as ariaAppHider from '../lib/helpers/ariaAppHider'; +import { + isBodyWithReactModalOpenClass, findDOMWithClass, + contentAttribute, overlayAttribute, + mcontent, moverlay, + clickAt, mouseDownAt, mouseUpAt, escKeyDown, tabKeyDown, + renderModal, unmountModal, emptyDOM +} from './helper'; + +describe('Style', () => { + afterEach('Unmount modal', emptyDOM); + + it('overrides the default styles when a custom classname is used', () => { + const modal = renderModal({ isOpen: true, className: 'myClass' }); + expect(mcontent(modal).style.top).toEqual(''); + }); + + it('overrides the default styles when a custom overlayClassName is used', () => { + const modal = renderModal({ + isOpen: true, + overlayClassName: 'myOverlayClass' + }); + expect(moverlay(modal).style.backgroundColor).toEqual(''); + }); + + it('supports adding style to the modal contents', () => { + const style = { content: { width: '20px' } }; + const modal = renderModal({ isOpen: true, style: style }); + expect(mcontent(modal).style.width).toEqual('20px'); + }); + + it('supports overriding style on the modal contents', () => { + const style = { content: { position: 'static' } }; + const modal = renderModal({ isOpen: true, style: style }); + expect(mcontent(modal).style.position).toEqual('static'); + }); + + it('supports adding style on the modal overlay', () => { + const style = { overlay: { width: '75px' } }; + const modal = renderModal({ isOpen: true, style: style }); + expect(moverlay(modal).style.width).toEqual('75px'); + }); + + it('supports overriding style on the modal overlay', () => { + const style = { overlay: { position: 'static' } }; + const modal = renderModal({ isOpen: true, style: style }); + expect(moverlay(modal).style.position).toEqual('static'); + }); + + it('supports overriding the default styles', () => { + const previousStyle = Modal.defaultStyles.content.position; + // Just in case the default style is already relative, check that we can change it + const newStyle = previousStyle === 'relative' ? 'static' : 'relative'; + Modal.defaultStyles.content.position = newStyle; + const modal = renderModal({ isOpen: true }); + expect(modal.portal.refs.content.style.position).toEqual(newStyle); + Modal.defaultStyles.content.position = previousStyle; + }); +}); diff --git a/specs/helper.js b/specs/helper.js index f50fbab2..e34bf37a 100644 --- a/specs/helper.js +++ b/specs/helper.js @@ -1,10 +1,109 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Modal from '../lib/components/Modal'; - +import TestUtils from 'react-addons-test-utils'; const divStack = []; +/** + * Polyfill for String.includes on some node versions. + */ +if (!(String.prototype.hasOwnProperty('includes'))) { + String.prototype.includes = function(item) { + return this.length > 0 && this.split(" ").indexOf(item) !== -1; + }; +} + +/** + * Check if the document.body contains the react modal + * open class. + * @return {Boolean} + */ +export const isBodyWithReactModalOpenClass = () => + document.body.className.includes('ReactModal__Body--open'); + +/** + * Returns a rendered dom element by class. + * @param {React} element A react instance. + * @param {String} className A class to find. + * @return {DOMElement} + */ +export const findDOMWithClass = TestUtils.findRenderedDOMComponentWithClass; + +/** + * Returns an attribut of a rendered react tree. + * @param {React} component A react instance. + * @return {String} + */ +const getModalAttribute = component => (instance, attr) => + modalComponent(component)(instance).getAttribute(attr); + +/** + * Return an element from a react component. + * @param {React} A react instance. + * @return {DOMElement} + */ +const modalComponent = component => instance => + instance.portal.refs[component]; + +/** + * Returns the modal content. + * @param {Modal} modal Modal instance. + * @return {DOMElement} + */ +export const mcontent = modalComponent('content'); + +/** + * Returns the modal overlay. + * @param {Modal} modal Modal instance. + * @return {DOMElement} + */ +export const moverlay = modalComponent('overlay'); + +/** + * Return an attribute of modal content. + * @param {Modal} modal Modal instance. + * @return {String} + */ +export const contentAttribute = getModalAttribute('content'); + +/** + * Return an attribute of modal overlay. + * @param {Modal} modal Modal instance. + * @return {String} + */ +export const overlayAttribute = getModalAttribute('overlay'); + +const Simulate = TestUtils.Simulate; + +const dispatchMockEvent = eventCtor => (key, code) => (element, opts) => + eventCtor(element, Object.assign({}, { + key: key, keyCode: code, which: code + }, opts)); + +const dispatchMockKeyDownEvent = dispatchMockEvent(Simulate.keyDown); + +/** + * Dispatch an 'esc' key down event from an element. + */ +export const escKeyDown = dispatchMockKeyDownEvent("ESC", 27); +/** + * Dispatch a 'tab' key down event from an element. + */ +export const tabKeyDown = dispatchMockKeyDownEvent("TAB", 9); +/** + * Dispatch a 'click' event at a node. + */ +export const clickAt = Simulate.click; +/** + * Dispatch a 'mouse up' event at a node. + */ +export const mouseUpAt = Simulate.mouseUp; +/** + * Dispatch a 'mouse down' event at a node. + */ +export const mouseDownAt = Simulate.mouseDown; + export const renderModal = function(props, children, callback) { props.ariaHideApp = false; const currentDiv = document.createElement('div');