Skip to content

Commit

Permalink
TASK: Refactor the executeCallback abstract and add unit tests. #18
Browse files Browse the repository at this point in the history
  • Loading branch information
Inkdpixels committed Jan 15, 2016
1 parent 552716e commit 06f6bfa
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 34 deletions.
50 changes: 35 additions & 15 deletions Resources/Private/JavaScript/Host/Abstracts/ExecuteCallback.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
import isObject from 'lodash.isobject';
import isFunction from 'lodash.isfunction';

export const ERROR_INVALID_EVENT = 'Please supply a valid native or Synthetic Event to the executeCallback function.';

/**
* Abstracts the prevent of a DOM event and the existence check for a callback function.
* @param {Event} e The event object to handle.
* @param {Function} cb The callback to call after the event was handled.
* @param {Boolean} preventDefault Should the function call e.preventDefault();? (Defaults to `true`)
* @param {Boolean} stopImmediatePropagation Should the function call e.stopImmediatePropagation();? (Defaults to `false`)
*
* @param {Array} ...args All arguments which are passed to the function.
* @return {Null}
* @example <a onClick={e => executeCallback(e, this.props.onClick)}>Click me</a>
*/
const executeCallback = (...args) => {
let e;
export default ({e, cb, preventDefault = true, stopImmediatePropagation = false} = {}) => {
//
// Check for the validaty of the event object, if one was passed.
//
if (e && !e.preventDefault) {
throw new Error(ERROR_INVALID_EVENT);
}

args.forEach(arg => {
if (isObject(arg) && isFunction(arg.preventDefault)) {
e = arg;
e.preventDefault();
} else if (isFunction(arg)) {
arg(e);
}
});
};
if (e && preventDefault) {
e.preventDefault();
}

export default executeCallback;
if (e && stopImmediatePropagation) {
//
// Since react bubbles SyntheticEvents instead of native DOM events,
// we need to handle both cases appropriately.
//
// @see https://facebook.github.io/react/docs/events.html#syntheticevent
//
try {
e.stopImmediatePropagation();
} catch(e) {}
try {
e.stopPropagation();
} catch(e) {}
}

if (cb && isFunction(cb)) {
cb(e);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import chai from 'chai';
import sinon from 'sinon';
import sinonMiddleware from 'sinon-chai';
import executeCallback, {ERROR_INVALID_EVENT} from './ExecuteCallback.js';

const expect = chai.expect;

chai.should();
chai.use(sinonMiddleware);

describe('"ExecuteCallback" abstract', () => {
let eventMock = null;

beforeEach(done => {
eventMock = {
preventDefault: sinon.spy(),
stopImmediatePropagation: sinon.spy(),
stopPropagation: sinon.spy()
};

done();
});

afterEach(done => {
eventMock = null;

done();
});

it('should call the `cb` property if it was passed to the function.', () => {
const spy = sinon.spy();

executeCallback({
cb: spy
});

spy.should.have.callCount(1);
});

it('should throw an error if an invalid event object was passed to the function.', () => {
const spy = sinon.spy();

const fn = () => executeCallback({
cb: spy,
e: {
nope: true
}
});;

expect(fn).to.throw(ERROR_INVALID_EVENT);
});

it('should propagate the event object to the `cb` function.', () => {
const spy = sinon.spy();

executeCallback({
cb: spy,
e: eventMock
});

spy.should.have.been.calledWith(eventMock);
});

it('should call the preventDefault() method if not otherwise configured.', () => {
executeCallback({
e: eventMock
});

eventMock.preventDefault.should.have.callCount(1);
});

it('should not call the preventDefault() method if properly configured.', () => {
executeCallback({
e: eventMock,
preventDefault: false
});

eventMock.preventDefault.should.have.callCount(0);
});

it('should call either the stopPropagation() method if properly configured.', () => {
executeCallback({
e: eventMock,
stopImmediatePropagation: true
});

eventMock.stopPropagation.should.have.callCount(1);
eventMock.stopImmediatePropagation.should.have.callCount(1);
});
});
10 changes: 5 additions & 5 deletions Resources/Private/JavaScript/Host/Components/Button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ const Button = props => {
});
const attributes = {
className: classNames,
onClick: e => executeCallback(e, onClick),
onMouseDown: e => executeCallback(e, onMouseDown),
onMouseUp: e => executeCallback(e, onMouseUp),
onMouseEnter: e => executeCallback(e, onMouseEnter),
onMouseLeave: e => executeCallback(e, onMouseLeave),
onClick: e => executeCallback({e, cb: onClick}),
onMouseDown: e => executeCallback({e, cb: onMouseDown}),
onMouseUp: e => executeCallback({e, cb: onMouseUp}),
onMouseEnter: e => executeCallback({e, cb: onMouseEnter}),
onMouseLeave: e => executeCallback({e, cb: onMouseLeave}),
ref: btn => {
const method = isFocused ? 'focus' : 'blur';

Expand Down
5 changes: 3 additions & 2 deletions Resources/Private/JavaScript/Host/Components/Dialog/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const Dialog = props => {
const {
className,
children,
isOpen
isOpen,
onRequestClose
} = props;
const classNames = mergeClassNames({
[style.dialog]: true,
Expand All @@ -24,7 +25,7 @@ const Dialog = props => {
<IconButton
icon="close"
className={style.dialog__contents__inner__closeBtn}
onClick={e => executeCallback(e, props.onRequestClose)}
onClick={e => executeCallback({e, cb: onRequestClose})}
/>

{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default class DropDown extends Component {
<div className={dropDownClassName} ref="dropDown">
<button
className={buttonClassName}
onClick={e => executeCallback(e, this.toggleDropDown.bind(this))}
onClick={e => executeCallback({e, cb: this.toggleDropDown.bind(this)})}
ref={btn => {
const method = isOpened ? 'focus' : 'blur';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, {Component, PropTypes} from 'react';
import mergeClassNames from 'classnames';
import {executeCallback} from 'Host/Abstracts/';
import {service} from 'Shared/';
import Icon from 'Host/Components/Icon/';
import Button from 'Host/Components/Button/';
Expand Down Expand Up @@ -101,7 +100,7 @@ export default class IconButtonDropDown extends Component {
this.cancelHoldTimeout();

if (!isOpened) {
executeCallback(this.props.onClick);
this.props.onClick();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ export default class Tabs extends Component {
[style.tabs__navigation__item]: true,
[style['tabs__navigation__item--isActive']]: this.state.activeTab === (index)
});
const onClick = e => executeCallback({e, cb: () => this.activateTabForIndex(index)});

return (
<li ref={ref} key={index} className={classes}>
<a onClick={e => executeCallback(e, () => this.activateTabForIndex(index))}>
<a onClick={onClick}>
{icon ? <Icon icon={icon} /> : null}
{title}
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ const TextInput = props => {
id={id}
type="text"
placeholder={placeholder}
onChange={e => onChangeHandler(e, onChange)}
onFocus={() => executeCallback(onFocus)}
onBlur={() => executeCallback(onBlur)}
onChange={e => onChangeHandler({e, cb: onChange})}
onFocus={() => executeCallback({cb: onFocus})}
onBlur={() => executeCallback({cb: onBlur})}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default class FooterBar extends Component {
ban: severity.toLowerCase() === 'error',
info: severity.toLowerCase() === 'info'
}) || 'info';
const onClick = e => executeCallback({
e,
cb: () => this.commenceClose()
});

return (
<div className={flashMessageClasses}>
Expand All @@ -44,7 +48,7 @@ export default class FooterBar extends Component {
className={style.flashMessage__btnClose}
style="transparent"
hoverStyle="darken"
onClick={e => executeCallback(e, this.commenceClose.bind(this))}
onClick={onClick}
/>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"css-loader": "^0.23.1",
"extract-text-webpack-plugin": "^0.9.1",
"glob": "^6.0.4",
"imports-loader": "^0.6.5",
"jsdom": "^7.2.2",
"mocha": "^2.3.4",
"postcss-import": "^7.1.3",
Expand All @@ -20,6 +21,8 @@
"postcss-simple-vars": "^1.2.0",
"react-addons-test-utils": "^0.14.6",
"redux-devtools": "^2.1.3",
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.9"
Expand All @@ -37,7 +40,6 @@
"lodash.curry": "^3.0.2",
"lodash.debounce": "^4.0.0",
"lodash.isfunction": "^3.0.6",
"lodash.isobject": "^3.0.2",
"medium-editor": "^5.10.0",
"normalize.css": "^3.0.3",
"react": "^0.14.6",
Expand Down
3 changes: 1 addition & 2 deletions webpack.shared.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ module.exports = {
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader')
}
],
postLoaders: []
]
},

postcss: [
Expand Down
28 changes: 27 additions & 1 deletion webpack.testing.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ const config = require('./webpack.shared.config.js');
//
const tests = glob.sync('Resources/Private/JavaScript/**/*.spec.js').map(test => `./${test}`);

//
// Workaround for sinon since it requires itself, and webpack can't find the circular dependencies.
//
// @see https://github.com/webpack/webpack/issues/177
//
const loaders = Object.create(config.module.loaders);
const resolve = Object.create(config.resolve);
loaders.push({
test: /sinon\.js$/,
loader: "imports?define=>false,require=>false"
});

if (!resolve.alias) {
resolve.alias = {};
}
resolve.alias.sinon = 'sinon/pkg/sinon';

//
// Export the webpack configuration for the test environment.
//
module.exports = Object.assign({}, config, {
entry: {
tests
Expand All @@ -15,5 +35,11 @@ module.exports = Object.assign({}, config, {
output: {
filename: 'JavaScript/Tests.js',
path: path.resolve('./Resources/Public/')
}
},

module: {
loaders
},

resolve
});

0 comments on commit 06f6bfa

Please sign in to comment.