diff --git a/.babelrc b/.babelrc index 76ab698..4ad2886 100644 --- a/.babelrc +++ b/.babelrc @@ -15,5 +15,10 @@ } ] ] - ] + ], + "env": { + "test": { + "plugins": ["istanbul"] + } + } } diff --git a/.gitignore b/.gitignore index cdcd44e..e5d46d5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ umd node_modules npm-debug.log coverage +.nyc_output \ No newline at end of file diff --git a/package.json b/package.json index a1e5b0e..76a2090 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,17 @@ "build-es6": "NODE_ENV=production webpack --output-library-target commonjs2 src/index.js ./es6/index.js", "build-umd": "NODE_ENV=production webpack src/index.js umd/stormpath-sdk-react.js", "build-min": "NODE_ENV=production webpack -p src/index.js umd/stormpath-sdk-react.min.js", - "build-dist": "NODE_ENV=production webpack src/index.js dist/stormpath-sdk-react.js && NODE_ENV=production webpack -p src/index.js dist/stormpath-sdk-react.min.js" + "build-dist": "NODE_ENV=production webpack src/index.js dist/stormpath-sdk-react.js && NODE_ENV=production webpack -p src/index.js dist/stormpath-sdk-react.min.js", + "test": "better-npm-run test", + "test:watch": "mocha -w test/helpers/browser.js test/*.spec.js test/**/*.spec.js" + }, + "betterScripts": { + "test": { + "env": { + "NODE_ENV": "test" + }, + "command": "nyc _mocha --report html -- test/helpers/browser.js test/*.spec.js test/**/*.spec.js" + } }, "devDependencies": { "babel-cli": "^6.18.0", @@ -33,14 +43,20 @@ "babel-eslint": "^7.1.0", "babel-loader": "^6.2.7", "babel-plugin-dev-expression": "^0.2.1", + "babel-plugin-istanbul": "^4.0.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-es2015-classes": "^6.18.0", "babel-plugin-transform-proto-to-assign": "^6.9.0", "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.16.0", + "babel-register": "^6.23.0", + "better-npm-run": "0.0.14", "bundle-loader": "^0.5.4", + "chai": "^3.5.0", + "chai-spies": "^0.7.1", "css-loader": "^0.25.0", + "enzyme": "^2.7.1", "eslint": "^3.9.1", "eslint-plugin-react": "^6.6.0", "fbjs": "^0.8.5", @@ -50,12 +66,16 @@ "history": "^2.1.2", "invariant": "^2.2.1", "isparta-loader": "^2.0.0", + "istanbul": "^0.4.5", + "jsdom": "^9.11.0", "keymirror": "^0.1.1", + "mocha": "^3.2.0", + "nyc": "^10.1.2", "pretty-bytes": "^4.0.2", "qs": "^6.3.0", "react": "^15.3.2", "react-addons-css-transition-group": "^15.3.2", - "react-addons-test-utils": "15.3.2", + "react-addons-test-utils": "^15.3.2", "react-dom": "^15.3.2", "react-router": "^3.0.0", "react-static-container": "^1.0.1", @@ -80,5 +100,29 @@ ], "dependencies": { "xtend": "^4.0.1" + }, + "nyc": { + "lines": "20", + "statements": "20", + "branches": "20", + "functions": "20", + "sourceMap": false, + "instrument": false, + "include": "src/**/*.js", + "exclude": [ + "dist/**/*", + "es6/**/*", + "lib/**/*", + "test/**/*", + "umd/**/*" + ], + "reporter": [ + "lcov", + "text-summary" + ], + "check-coverage": true, + "require": [ + "./test/helpers/browser.js" + ] } } diff --git a/src/actions/TokenActions.js b/src/actions/TokenActions.js index 23358e3..5d20f9b 100644 --- a/src/actions/TokenActions.js +++ b/src/actions/TokenActions.js @@ -1,6 +1,7 @@ import context from './../context'; import TokenConstants from './../constants/TokenConstants'; +/* istanbul ignore next */ function dispatch(event) { setTimeout(() => { context.getDispatcher().dispatch(event); @@ -8,8 +9,17 @@ function dispatch(event) { } class TokenActions { + constructor(dispatch) { + this.dispatch = dispatch; + } + + // Allows for setting mock dispatch in tests + setDispatch(dispatch) { + this.dispatch = dispatch; + } + set(type, token, callback) { - dispatch({ + this.dispatch({ type: TokenConstants.TOKEN_SET, options: { type: type, @@ -20,7 +30,7 @@ class TokenActions { } refresh(token, callback) { - dispatch({ + this.dispatch({ type: TokenConstants.TOKEN_REFRESH, options: { token: token @@ -30,4 +40,4 @@ class TokenActions { } } -export default new TokenActions() +export default new TokenActions(dispatch) diff --git a/src/components/Authenticated.js b/src/components/Authenticated.js index ddadf7f..ddb451a 100644 --- a/src/components/Authenticated.js +++ b/src/components/Authenticated.js @@ -11,6 +11,7 @@ export default class Authenticated extends React.Component { let authenticated = user !== undefined; if (authenticated && this.props.inGroup) { + /* istanbul ignore else */ if (user.groups) { authenticated = utils.groupsMatchExpression(user.groups, this.props.inGroup); } else { diff --git a/src/context.js b/src/context.js index 02a6e33..fe09321 100644 --- a/src/context.js +++ b/src/context.js @@ -29,6 +29,10 @@ class Context { this.tokenStore = tokenStore; } + getTokenStore() { + return this.tokenStore; + } + setSessionStore(sessionStore) { this.sessionStore = sessionStore; } diff --git a/src/dispatchers/FluxDispatcher.js b/src/dispatchers/FluxDispatcher.js index f185ccc..841dd31 100644 --- a/src/dispatchers/FluxDispatcher.js +++ b/src/dispatchers/FluxDispatcher.js @@ -1,8 +1,11 @@ import { Dispatcher } from 'flux'; +// Extracted for testability +const defaultDispatcher = new Dispatcher(); + export default class FluxDispatcher { - constructor(reducer) { - this.dispatcher = new Dispatcher(); + constructor(reducer, dispatcher = defaultDispatcher) { + this.dispatcher = dispatcher; this.register(reducer); } diff --git a/test/ContextTest.js b/test/ContextTest.js deleted file mode 100644 index e69de29..0000000 diff --git a/test/actions/TokenActions.spec.js b/test/actions/TokenActions.spec.js new file mode 100644 index 0000000..c39cb9f --- /dev/null +++ b/test/actions/TokenActions.spec.js @@ -0,0 +1,54 @@ +import TokenActions from '../../src/actions/TokenActions'; +import TokenConstants from '../../src/constants/TokenConstants'; +import chai, {expect} from 'chai'; +import spies from 'chai-spies'; + +chai.use(spies); + +describe('TokenActions', () => { + let dispatchSpy; + let callback; + + beforeEach(() => { + dispatchSpy = chai.spy(({callback}) => callback()); + callback = () => ({}); + + TokenActions.setDispatch(dispatchSpy); + }); + + describe('set action', () => { + it('should dispatch a token set action', () => { + const type = 'sometype'; + const token = 'sometoken'; + + TokenActions.set(type, token, callback); + + expect(dispatchSpy).to.have.been.calledOnce; + expect(dispatchSpy).to.have.been.called.with({ + type: TokenConstants.TOKEN_SET, + options: { + type, + token, + }, + callback, + }); + }); + }); + + describe('refresh action', () => { + it('should dispatch a refresh action', () => { + const token = 'sometoken'; + + TokenActions.refresh(token, callback); + + expect(dispatchSpy).to.have.been.calledOnce; + expect(dispatchSpy).to.have.been.called.with({ + type: TokenConstants.TOKEN_REFRESH, + options: { + token, + }, + callback, + }); + }); + }); +}); diff --git a/test/components/Authenticated.spec.js b/test/components/Authenticated.spec.js new file mode 100644 index 0000000..9a96f91 --- /dev/null +++ b/test/components/Authenticated.spec.js @@ -0,0 +1,82 @@ +import React from 'react'; +import {shallow} from 'enzyme'; +import {expect} from 'chai'; + +import Authenticated from '../../src/components/Authenticated'; + +describe('', () => { + let userContext; + let noUserContext; + + before(() => { + userContext = { + user: { + groups: { + basic: true, + unbasic: true + } + } + }; + + noUserContext = {}; + }); + + describe('without inGroup property', () => { + it('should render content if there is a user', () => { + const element = shallow( + Shown, + {context: userContext} + ); + + expect(element).to.be.ok; + expect(element.text()).to.equal('Shown'); + }); + + it('should not render content if there is no user', () => { + const element = shallow( + Not Shown, + {context: noUserContext} + ); + + expect(element).to.be.ok; + expect(element.text()).not.to.be.ok; + expect(element.text()).not.to.equal('Not Shown'); + }); + }); + + + describe('with inGroup property', () => { + it('should not render content if there is no user', () => { + const element = shallow( + Not Shown, + {context: noUserContext} + ); + + expect(element).to.be.ok; + expect(element.text()).not.to.be.ok; + expect(element.text()).not.to.equal('Not Shown'); + }); + + it('should not render content if the user is not in the group', () => { + const element = shallow( + Not Shown, + {context: userContext} + ); + + expect(element).to.be.ok; + expect(element.text()).not.to.be.ok; + expect(element.text()).not.to.equal('Not Shown'); + }); + + it('should render content if the user is in the group', () => { + const element = shallow( + Shown, + {context: userContext} + ); + + expect(element).to.be.ok; + expect(element.text()).to.be.ok; + expect(element.text()).to.equal('Shown'); + }); + }); +}); diff --git a/test/context.spec.js b/test/context.spec.js new file mode 100644 index 0000000..dc2011e --- /dev/null +++ b/test/context.spec.js @@ -0,0 +1,38 @@ +import chai, {expect} from 'chai'; + +import context from '../src/context'; + +describe('Context', () => { + it('should have empty initial values', () => { + expect(context.router).not.to.be.ok; + expect(context.dispatcher).not.to.be.ok; + expect(context.tokenStore).not.to.be.ok; + expect(context.sessionStore).not.to.be.ok; + expect(context.userStore).not.to.be.ok; + }); + + it('should have setters for all values', () => { + context.setRouter('router'); + expect(context.router).to.equal('router'); + + context.setDispatcher('dispatcher'); + expect(context.dispatcher).to.equal('dispatcher'); + + context.setTokenStore('tokenStore'); + expect(context.tokenStore).to.equal('tokenStore'); + + context.setSessionStore('sessionStore'); + expect(context.sessionStore).to.equal('sessionStore'); + + context.setUserStore('userStore'); + expect(context.userStore).to.equal('userStore'); + }); + + it('should have getters for all values', () => { + expect(context.router).to.equal(context.getRouter()); + expect(context.dispatcher).to.equal(context.getDispatcher()); + expect(context.tokenStore).to.equal(context.getTokenStore()); + expect(context.sessionStore).to.equal(context.getSessionStore()); + expect(context.userStore).to.equal(context.getUserStore()); + }); +}); diff --git a/test/dispatchers/FluxDispatcher.spec.js b/test/dispatchers/FluxDispatcher.spec.js new file mode 100644 index 0000000..9d83628 --- /dev/null +++ b/test/dispatchers/FluxDispatcher.spec.js @@ -0,0 +1,55 @@ +import chai, {expect} from 'chai'; +import spies from 'chai-spies'; + +import FluxDispatcher from '../../src/dispatchers/FluxDispatcher'; + +chai.use(spies); + +describe('FluxDispatcher', () => { + let reducer; + let dispatcher; + let fluxDispatcher; + + beforeEach(function() { + reducer = chai.spy(); + + dispatcher = { + register: chai.spy(), + dispatch: chai.spy(), + }; + + fluxDispatcher = new FluxDispatcher(reducer, dispatcher); + }); + + describe('constructor', () => { + it('should register the reducer to the dispatcher when constructed', () => { + expect(dispatcher.register).to.have.been.calledOnce; + expect(fluxDispatcher.dispatcher).to.equal(dispatcher); + }); + }); + + describe('dispatch method', () => { + let event; + + before(() => { + event = { + type: 'eventType', + options: { + optionable: true + }, + callback: () => ({}) + }; + }); + + it('should call the set dispatcher with the destructured data', () => { + fluxDispatcher.dispatch(event); + + expect(dispatcher.dispatch).to.have.been.calledOnce; + expect(dispatcher.dispatch).to.have.been.called.with.exactly({ + actionType: event.type, + options: event.options, + callback: event.callback, + }); + }); + }); +}); diff --git a/test/dispatchers/ReduxDispatcher.spec.js b/test/dispatchers/ReduxDispatcher.spec.js new file mode 100644 index 0000000..e0c248b --- /dev/null +++ b/test/dispatchers/ReduxDispatcher.spec.js @@ -0,0 +1,34 @@ +import chai, {expect} from 'chai'; +import spies from 'chai-spies'; + +import ReduxDispatcher from '../../src/dispatchers/ReduxDispatcher'; + +chai.use(spies); + +describe('ReduxDispatcher', () => { + let reducer; + let store; + let reduxDispatcher; + let event; + + before(() => { + reducer = chai.spy(); + store = { + dispatch: chai.spy() + }; + + reduxDispatcher = new ReduxDispatcher(reducer, store); + + event = {type: 'event'}; + }); + + it('should call the reducer and dispatch the event on .dispatch()', () => { + reduxDispatcher.dispatch(event); + + expect(reducer).to.have.been.calledOnce; + expect(reducer).to.have.been.called.with.exactly(event); + + expect(store.dispatch).to.have.been.calledOnce; + expect(store.dispatch).to.have.been.called.with.exactly(event); + }); +}); diff --git a/test/helpers/browser.js b/test/helpers/browser.js new file mode 100644 index 0000000..5bfa72b --- /dev/null +++ b/test/helpers/browser.js @@ -0,0 +1,21 @@ +// Taken from https://semaphoreci.com/community/tutorials/testing-react-components-with-enzyme-and-mocha +require('babel-register')(); + +var jsdom = require('jsdom').jsdom; + +var exposedProperties = ['window', 'navigator', 'document']; + +global.document = jsdom(''); +global.window = document.defaultView; +Object.keys(document.defaultView).forEach((property) => { + if (typeof global[property] === 'undefined') { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } +}); + +global.navigator = { + userAgent: 'node.js' +}; + +global.documentRef = document; diff --git a/test/helpers/storage-mock.js b/test/helpers/storage-mock.js new file mode 100644 index 0000000..e546876 --- /dev/null +++ b/test/helpers/storage-mock.js @@ -0,0 +1,27 @@ +import chai from 'chai'; +import spies from 'chai-spies'; + +chai.use(spies); + +export default class StorageMock { + constructor(name) { + this.name = name; + this._storage = {}; + + this.getItem = chai.spy(this._get.bind(this)); + this.setItem = chai.spy(this._set.bind(this)); + this.removeItem = chai.spy(this._remove.bind(this)); + } + + _get(name) { + return this._storage[name]; + } + + _set(name, value) { + this._storage[name] = String(value); + } + + _remove(name) { + delete this._storage[name]; + } +} diff --git a/test/storage/LocalStorage.spec.js b/test/storage/LocalStorage.spec.js new file mode 100644 index 0000000..9d1a923 --- /dev/null +++ b/test/storage/LocalStorage.spec.js @@ -0,0 +1,113 @@ +import chai, {expect} from 'chai'; +import spies from 'chai-spies'; + +import StorageMock from '../helpers/storage-mock'; +import LocalStorage from '../../src/storage/LocalStorage'; + +chai.use(spies); + +describe('LocalStorage storage', () => { + before(() => { + global.localStorage = new StorageMock('local'); + global.sessionStorage = new StorageMock('session'); + }); + + describe('constructor', () => { + it('should use sessionStorage if type is not `local`', () => { + let storage = new LocalStorage(); + let storageOther = new LocalStorage('something'); + + expect(storage.storage).to.equal(global.sessionStorage); + expect(storageOther.storage).to.equal(global.sessionStorage); + expect(storage.storage).not.to.equal(global.localStorage); + expect(storageOther.storage).not.to.equal(global.localStorage); + }); + + it('should use localStorage if type is `local`', () => { + let storage = new LocalStorage('local'); + + expect(storage.storage).to.equal(global.localStorage); + expect(storage.storage).not.to.equal(global.sessionStorage); + }); + }); + + describe('with storage present on device/browser', () => { + describe('get', () => { + const key = 'key'; + const value = 'value'; + + it('should retrieve an item by name from local storage', (done) => { + const storage = new LocalStorage(); + + global.sessionStorage._set(key, value); + + storage.get(key).then((storageValue) => { + expect(storageValue).to.equal(value); + done(); + }).catch(done); + }); + }); + + describe('set', () => { + const key = 'key'; + const value = 123; + + it('should store an item by name in local storage, stringifying it', (done) => { + const storage = new LocalStorage(); + + storage.set(key, value).then(() => { + expect(global.sessionStorage._get(key)).not.to.equal(value); + expect(global.sessionStorage._get(key)).to.equal(String(value)); + done(); + }).catch(done); + }); + }); + + describe('remove', () => { + const key = 'key'; + const value = 'value'; + + it('should remove an item by name from local storage', (done) => { + const storage = new LocalStorage(); + global.sessionStorage._set(key, value); + + expect(global.sessionStorage._get(key)).to.equal(value); + + storage.remove(key).then(() => { + expect(global.sessionStorage._get(key)).not.to.be.ok; + done(); + }).catch(done); + }); + }); + }); + + describe('without storage present on device/browser', () => { + let storage; + before(function() { + global.sessionStorage = null; + storage = new LocalStorage(); + }); + + it('should throw on get', (done) => { + storage.get('key').catch((err) => { + expect(err).to.be.ok; + done(); + }); + }); + + it('should throw on set', (done) => { + storage.set('key', 'value').catch((err) => { + expect(err).to.be.ok; + done(); + }); + }); + + it('should throw on remove', (done) => { + storage.remove('key').catch((err) => { + expect(err).to.be.ok; + done(); + }); + }); + }); + +}); diff --git a/test/stores/BaseStore.spec.js b/test/stores/BaseStore.spec.js new file mode 100644 index 0000000..bddda37 --- /dev/null +++ b/test/stores/BaseStore.spec.js @@ -0,0 +1,45 @@ +import chai, {expect} from 'chai'; +import spies from 'chai-spies'; + +import BaseStore from '../../src/stores/BaseStore'; + +chai.use(spies); + +describe('BaseStore', () => { + let store; + + before(() => { + store = new BaseStore(); + + chai.spy.on(store, 'emit'); + chai.spy.on(store, 'on'); + chai.spy.on(store, 'removeListener'); + }); + + it('should emit a `changed` event on .emitChange()', () => { + const value = value; + + store.emitChange(value); + + expect(store.emit).to.have.been.calledOnce; + expect(store.emit).to.have.been.called.with.exactly('changed', value); + }); + + it('should add a `changed` event listener on .addChangeListener()', () => { + const cb = () => ({}); + + store.addChangeListener(cb); + + expect(store.on).to.have.been.calledOnce; + expect(store.on).to.have.been.called.with.exactly('changed', cb); + }); + + it('should remove a `changed` event listener on .removeChangeListener()', () => { + const cb = () => ({}); + + store.removeChangeListener(cb); + + expect(store.removeListener).to.have.been.calledOnce; + expect(store.removeListener).to.have.been.called.with.exactly('changed', cb); + }); +}); diff --git a/test/stores/SessionStore.spec.js b/test/stores/SessionStore.spec.js new file mode 100644 index 0000000..a617d1c --- /dev/null +++ b/test/stores/SessionStore.spec.js @@ -0,0 +1,147 @@ +import chai, {expect} from 'chai'; +import spies from 'chai-spies'; + +chai.use(spies); + +import SessionStore from '../../src/stores/SessionStore'; + +describe('SessionStore', () => { + let sessionStore; + + describe('.set(session)', () => { + let session; + let otherSession; + + beforeEach(() => { + sessionStore = new SessionStore(); + chai.spy.on(sessionStore, 'emitChange'); + + sessionStore.session = undefined; + + session = { + name: 'A', + }; + + otherSession = { + groups: { + href: 'groupHref', + items: [ + { + name: 'a', + status: 'ENABLED' + }, + { + name: 'b', + status: 'DISABLED' + } + ] + }, + name: 'B', + }; + + }); + + it('should set the session', () => { + expect(sessionStore.session).not.to.be.ok; + + sessionStore.set(session); + + expect(sessionStore.session).to.be.ok; + expect(sessionStore.session.name).to.equal(session.name); + }); + + it('should use only enabled groups', () => { + sessionStore.set(otherSession); + + expect(sessionStore.session).to.be.ok; + expect(sessionStore.session.groups).to.deep.equal({ + a: true + }); + }); + + it('should emit a changed event when a new session is set', () => { + sessionStore.set(session); + expect(sessionStore.emitChange).to.have.been.calledOnce; + expect(sessionStore.emitChange).to.have.been.called.with.exactly(session); + }); + + it('should not emit a changed event when the smae session is set', () => { + sessionStore.set(session); + sessionStore.emitChange.reset(); // Resets the spy internals + + sessionStore.set(session); + + expect(sessionStore.emitChange).not.to.have.been.called; + }); + }); + + describe('.get()', () => { + let session; + + before(() => { + sessionStore = new SessionStore(); + session = {name: 'session'}; + sessionStore.set(session); + }); + + it('should retrieve the session', () => { + expect(sessionStore.get()).to.equal(session); + }); + }); + + describe('.empty()', () => { + let session; + + before(() => { + sessionStore = new SessionStore(); + session = {name: 'session'}; + }); + + it('should return true if the session is empty', () => { + expect(sessionStore.empty()).to.be.true; + }); + + it('should return false if the session is set', () => { + sessionStore.set(session); + + expect(sessionStore.empty()).to.be.false; + }); + }); + + describe('.reset()', () => { + let session; + + beforeEach(() => { + sessionStore = new SessionStore(); + session = {name: 'session'}; + sessionStore.set(session); + }); + + it('should empty the session', () => { + expect(sessionStore.empty()).to.be.false; + + sessionStore.reset(); + + expect(sessionStore.empty()).to.be.true; + }); + + it('should emit an event if the sessionStore is not empty previously', () => { + chai.spy.on(sessionStore, 'emitChange'); + + sessionStore.reset(); + + expect(sessionStore.emitChange).to.have.been.calledOnce; + expect(sessionStore.emitChange).to.have.been.called.with.exactly(undefined); + }); + + it('should not emit an event if the session was empty previously', () => { + sessionStore.reset(); + expect(sessionStore.empty()).to.be.true; + + chai.spy.on(sessionStore, 'emitChange'); + sessionStore.reset(); + + expect(sessionStore.emitChange).not.to.have.been.called; + }); + }); +}); diff --git a/test/utils.spec.js b/test/utils.spec.js new file mode 100644 index 0000000..b87e111 --- /dev/null +++ b/test/utils.spec.js @@ -0,0 +1,75 @@ +import {expect} from 'chai'; +import {shallow} from 'enzyme'; +import React from 'react'; + +import utils from '../src/utils'; + +describe('utils', () => { + describe('nopElement', () => { + it('should produce an empty span', () => { + const element = shallow(utils.nopElement); + + expect(element).to.be.ok; + expect(element.type()).to.equal('span'); + expect(element.text()).not.to.be.ok; + }); + }); + + describe('uuid', () => { + it('should produce a unique string', () => { + const count = 10; + const uuids = []; + + for (let i = 0; i < count; i++) { + uuids.push(utils.uuid()); + } + + for(let i = 0; i < count; i++) { + expect(uuids[i]).to.be.a.string; + expect(uuids[i]).not.to.equal(uuids[(i + 1) % count]); + } + }); + }); + + describe('containsWord', () => { + it('should return true if the word if the testWord contains any of the given words', () => { + const testWord = 'this is just a test'; + const words = ['best', 'test', 'rest']; + + expect(utils.containsWord(testWord, words)).to.be.true; + }); + + it('should match partial matches (shorter word inside of longer word)', () => { + const testWord = 'this is just a testing run'; + const words = ['best', 'test', 'rest']; + + expect(utils.containsWord(testWord, words)).to.be.true; + }); + + it('should match words of different case', () => { + const testWord = 'this is just a test'.toUpperCase(); + const words = ['best', 'test', 'rest']; + + expect(utils.containsWord(testWord, words)).to.be.true; + }); + + it('should return false if none of the words are contained in the test string', () => { + const testWord = 'this is just an experimental run'; + const words = ['best', 'test', 'rest']; + + expect(utils.containsWord(testWord, words)).to.be.false; + }); + }); + + describe('takeProp', () => { + it('should take the first prop from the object it finds on the prop list', () => { + const data = {a: 1, b: 2, c:3}; + expect(utils.takeProp(data, 'a', 'b')).to.equal(data.a); + expect(utils.takeProp(data, 'b', 'a')).to.equal(data.b); + }); + + it('should return undefined if no props are matched', () => { + expect(utils.takeProp({a: 1, b: 2}, 'c', 'd', 'e', 'f')).to.be.undefined; + }); + }); +});