From 7fa84fc74d87ef3c0c93bb9fcc722891a2c2625d Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 10:39:27 +0100 Subject: [PATCH 01/19] Add basic test setup --- package.json | 9 +++++++-- test/helpers/browser.js | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test/helpers/browser.js diff --git a/package.json b/package.json index a1e5b0e..237d0d8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "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": "mocha -w test/helpers/browser.js test/**/*.spec.js" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -40,7 +41,9 @@ "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.16.0", "bundle-loader": "^0.5.4", + "chai": "^3.5.0", "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 +53,14 @@ "history": "^2.1.2", "invariant": "^2.2.1", "isparta-loader": "^2.0.0", + "jsdom": "^9.11.0", "keymirror": "^0.1.1", + "mocha": "^3.2.0", "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", diff --git a/test/helpers/browser.js b/test/helpers/browser.js new file mode 100644 index 0000000..8d86c97 --- /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' +}; + +documentRef = document; From 55fc80e45e3ce1f29fe30981d8f3da5a3cd7383e Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 10:39:41 +0100 Subject: [PATCH 02/19] Add Authenticated component tests --- test/components/Authenticated.spec.js | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/components/Authenticated.spec.js diff --git a/test/components/Authenticated.spec.js b/test/components/Authenticated.spec.js new file mode 100644 index 0000000..1d7bc07 --- /dev/null +++ b/test/components/Authenticated.spec.js @@ -0,0 +1,82 @@ +import React from 'react'; +import {mount, 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'); + }); + }); +}); From f4cafc160aa72628054ea6cc36267238aadb98ba Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 11:01:10 +0100 Subject: [PATCH 03/19] Add spies --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 237d0d8..0010f6b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "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", - "test": "mocha -w test/helpers/browser.js test/**/*.spec.js" + "test": "mocha test/helpers/setup.js test/**/*.spec.js", + "test:watch": "mocha -w test/helpers/setup.js test/**/*.spec.js" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -42,6 +43,7 @@ "babel-preset-stage-0": "^6.16.0", "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", From 9555eef3fab7f161a4528852156269ef2c5c37dd Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 11:01:18 +0100 Subject: [PATCH 04/19] Add TokenActions test --- test/actions/TokenActions.spec.js | 37 +++++++++++++++++++++++++++++++ test/helpers/setup.js | 2 ++ test/helpers/spyConfig.js | 4 ++++ 3 files changed, 43 insertions(+) create mode 100644 test/actions/TokenActions.spec.js create mode 100644 test/helpers/setup.js create mode 100644 test/helpers/spyConfig.js diff --git a/test/actions/TokenActions.spec.js b/test/actions/TokenActions.spec.js new file mode 100644 index 0000000..74433ce --- /dev/null +++ b/test/actions/TokenActions.spec.js @@ -0,0 +1,37 @@ +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, + }); + }); + }); +}); diff --git a/test/helpers/setup.js b/test/helpers/setup.js new file mode 100644 index 0000000..c56362f --- /dev/null +++ b/test/helpers/setup.js @@ -0,0 +1,2 @@ +require('./browser.js'); +require('./spyConfig.js'); diff --git a/test/helpers/spyConfig.js b/test/helpers/spyConfig.js new file mode 100644 index 0000000..251afe7 --- /dev/null +++ b/test/helpers/spyConfig.js @@ -0,0 +1,4 @@ +import chai from 'chai'; +import spies from 'chai-spies'; + +chai.use(spies); From 77e85e2cf58189a3ac6882abd0765192834e86fa Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 12:21:11 +0100 Subject: [PATCH 05/19] Cleanup config --- package.json | 4 ++-- test/helpers/setup.js | 2 -- test/helpers/spyConfig.js | 4 ---- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 test/helpers/setup.js delete mode 100644 test/helpers/spyConfig.js diff --git a/package.json b/package.json index 0010f6b..ed7a94f 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "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", - "test": "mocha test/helpers/setup.js test/**/*.spec.js", - "test:watch": "mocha -w test/helpers/setup.js test/**/*.spec.js" + "test": "mocha test/helpers/browser.js test/**/*.spec.js", + "test:watch": "mocha -w test/helpers/browser.js test/**/*.spec.js" }, "devDependencies": { "babel-cli": "^6.18.0", diff --git a/test/helpers/setup.js b/test/helpers/setup.js deleted file mode 100644 index c56362f..0000000 --- a/test/helpers/setup.js +++ /dev/null @@ -1,2 +0,0 @@ -require('./browser.js'); -require('./spyConfig.js'); diff --git a/test/helpers/spyConfig.js b/test/helpers/spyConfig.js deleted file mode 100644 index 251afe7..0000000 --- a/test/helpers/spyConfig.js +++ /dev/null @@ -1,4 +0,0 @@ -import chai from 'chai'; -import spies from 'chai-spies'; - -chai.use(spies); From 227027661583b69fbc9aa24f64049596f3d89868 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 12:21:53 +0100 Subject: [PATCH 06/19] Adjust TokenActions for testability --- src/actions/TokenActions.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/actions/TokenActions.js b/src/actions/TokenActions.js index 23358e3..643674e 100644 --- a/src/actions/TokenActions.js +++ b/src/actions/TokenActions.js @@ -8,8 +8,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 +29,7 @@ class TokenActions { } refresh(token, callback) { - dispatch({ + this.dispatch({ type: TokenConstants.TOKEN_REFRESH, options: { token: token @@ -30,4 +39,4 @@ class TokenActions { } } -export default new TokenActions() +export default new TokenActions(dispatch) From 7ad70dbf72e594ccc61a1773428c356fbbef3096 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 12:22:05 +0100 Subject: [PATCH 07/19] Add FluxDispatcher tests --- src/dispatchers/FluxDispatcher.js | 7 +++- test/dispatchers/FluxDispatcher.spec.js | 55 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 test/dispatchers/FluxDispatcher.spec.js 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/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, + }); + }); + }); +}); From a0e5dbf4fd6fd0b247f04f2d3b294577883ea337 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 12:26:32 +0100 Subject: [PATCH 08/19] Add ReduxDispatcher test --- test/dispatchers/ReduxDispatcher.spec.js | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/dispatchers/ReduxDispatcher.spec.js 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); + }); +}); From 3d153640239215a5cdbeaf07b145c93ee8695a67 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 12:37:17 +0100 Subject: [PATCH 09/19] Add LocalStorage test --- test/helpers/storage-mock.js | 27 +++++++++++++++++++++++++++ test/storage/LocalStorage.spec.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 test/helpers/storage-mock.js create mode 100644 test/storage/LocalStorage.spec.js diff --git a/test/helpers/storage-mock.js b/test/helpers/storage-mock.js new file mode 100644 index 0000000..6e268cf --- /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..ec29a96 --- /dev/null +++ b/test/storage/LocalStorage.spec.js @@ -0,0 +1,31 @@ +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); + +global.localStorage = new StorageMock('local'); +global.sessionStorage = new StorageMock('session'); + +describe('LocalStorage storage', () => { + 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); + }); + }); +}); From 6970e10f39a859537c699d27a2256fd680921d99 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 13:10:15 +0100 Subject: [PATCH 10/19] Setup coverage reporting with nyc --- .babelrc | 7 ++++++- .gitignore | 1 + package.json | 26 +++++++++++++++++++++++++- test/helpers/browser.js | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) 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 ed7a94f..7230081 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "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", - "test": "mocha test/helpers/browser.js test/**/*.spec.js", + "test": "cross-env NODE_ENV=test nyc _mocha --report html -- test/helpers/browser.js test/**/*.spec.js", "test:watch": "mocha -w test/helpers/browser.js test/**/*.spec.js" }, "devDependencies": { @@ -35,6 +35,7 @@ "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", @@ -44,6 +45,7 @@ "bundle-loader": "^0.5.4", "chai": "^3.5.0", "chai-spies": "^0.7.1", + "cross-env": "^3.1.4", "css-loader": "^0.25.0", "enzyme": "^2.7.1", "eslint": "^3.9.1", @@ -55,9 +57,11 @@ "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", @@ -87,5 +91,25 @@ ], "dependencies": { "xtend": "^4.0.1" + }, + "nyc": { + "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/test/helpers/browser.js b/test/helpers/browser.js index 8d86c97..5bfa72b 100644 --- a/test/helpers/browser.js +++ b/test/helpers/browser.js @@ -18,4 +18,4 @@ global.navigator = { userAgent: 'node.js' }; -documentRef = document; +global.documentRef = document; From 825a828d9a7da47ba909f988c770825ca441c2d3 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 13:42:33 +0100 Subject: [PATCH 11/19] Improve test coverage --- src/actions/TokenActions.js | 1 + src/components/Authenticated.js | 1 + test/actions/TokenActions.spec.js | 17 +++++++++++ test/helpers/storage-mock.js | 2 +- test/storage/LocalStorage.spec.js | 48 +++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/actions/TokenActions.js b/src/actions/TokenActions.js index 643674e..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); 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/test/actions/TokenActions.spec.js b/test/actions/TokenActions.spec.js index 74433ce..c39cb9f 100644 --- a/test/actions/TokenActions.spec.js +++ b/test/actions/TokenActions.spec.js @@ -34,4 +34,21 @@ describe('TokenActions', () => { }); }); }); + + 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/helpers/storage-mock.js b/test/helpers/storage-mock.js index 6e268cf..e546876 100644 --- a/test/helpers/storage-mock.js +++ b/test/helpers/storage-mock.js @@ -14,7 +14,7 @@ export default class StorageMock { } _get(name) { - return this._storage(name); + return this._storage[name]; } _set(name, value) { diff --git a/test/storage/LocalStorage.spec.js b/test/storage/LocalStorage.spec.js index ec29a96..45469c3 100644 --- a/test/storage/LocalStorage.spec.js +++ b/test/storage/LocalStorage.spec.js @@ -28,4 +28,52 @@ describe('LocalStorage storage', () => { expect(storage.storage).not.to.equal(global.sessionStorage); }); }); + + 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); + }); + }); }); From 54844c531f862f90dd7013c470290c37bcfaaa76 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 13:50:44 +0100 Subject: [PATCH 12/19] Test lack of presence of storage for LocalStorage --- test/storage/LocalStorage.spec.js | 102 ++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/test/storage/LocalStorage.spec.js b/test/storage/LocalStorage.spec.js index 45469c3..9d1a923 100644 --- a/test/storage/LocalStorage.spec.js +++ b/test/storage/LocalStorage.spec.js @@ -6,10 +6,12 @@ import LocalStorage from '../../src/storage/LocalStorage'; chai.use(spies); -global.localStorage = new StorageMock('local'); -global.sessionStorage = new StorageMock('session'); - 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(); @@ -29,51 +31,83 @@ describe('LocalStorage storage', () => { }); }); - describe('get', () => { - const key = 'key'; - const value = 'value'; + 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(); + it('should retrieve an item by name from local storage', (done) => { + const storage = new LocalStorage(); - global.sessionStorage._set(key, value); + global.sessionStorage._set(key, value); - storage.get(key).then((storageValue) => { - expect(storageValue).to.equal(value); - done(); - }).catch(done); + storage.get(key).then((storageValue) => { + expect(storageValue).to.equal(value); + done(); + }).catch(done); + }); }); - }); - describe('set', () => { - const key = 'key'; - const value = 123; + 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(); + 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); + 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('remove', () => { - const key = 'key'; - const value = 'value'; + describe('without storage present on device/browser', () => { + let storage; + before(function() { + global.sessionStorage = null; + storage = new LocalStorage(); + }); - it('should remove an item by name from local storage', (done) => { - const storage = new LocalStorage(); - global.sessionStorage._set(key, value); + it('should throw on get', (done) => { + storage.get('key').catch((err) => { + expect(err).to.be.ok; + done(); + }); + }); - expect(global.sessionStorage._get(key)).to.equal(value); + it('should throw on set', (done) => { + storage.set('key', 'value').catch((err) => { + expect(err).to.be.ok; + done(); + }); + }); - storage.remove(key).then(() => { - expect(global.sessionStorage._get(key)).not.to.be.ok; + it('should throw on remove', (done) => { + storage.remove('key').catch((err) => { + expect(err).to.be.ok; done(); - }).catch(done); + }); }); }); + }); From 095c9498a9a0dd74d522d9ef56215306e874fb8f Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 13:58:36 +0100 Subject: [PATCH 13/19] Add tests for BaseStore --- test/stores/BaseStore.spec.js | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/stores/BaseStore.spec.js 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); + }); +}); From 905c572d41938274f8a32304c4f3322d211cb755 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 14:20:20 +0100 Subject: [PATCH 14/19] Add SessionStore tests --- test/stores/SessionStore.spec.js | 147 +++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 test/stores/SessionStore.spec.js 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; + }); + }); +}); From 7b51687fa1969df5ee71669d28a920deb52102e1 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 14:39:49 +0100 Subject: [PATCH 15/19] Fix issue where test/*.spec.js was not covered by faux-glob --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7230081..56be4e3 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "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", - "test": "cross-env NODE_ENV=test nyc _mocha --report html -- test/helpers/browser.js test/**/*.spec.js", - "test:watch": "mocha -w test/helpers/browser.js test/**/*.spec.js" + "test": "cross-env NODE_ENV=test nyc _mocha --report html -- test/helpers/browser.js test/*.spec.js test/**/*.spec.js", + "test:watch": "mocha -w test/helpers/browser.js test/*.spec.js test/**/*.spec.js" }, "devDependencies": { "babel-cli": "^6.18.0", From 18397fd65a53c1691ced8b6951e665dff72b4581 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 14:39:55 +0100 Subject: [PATCH 16/19] Add context tests --- src/context.js | 4 ++++ test/ContextTest.js | 0 test/context.spec.js | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) delete mode 100644 test/ContextTest.js create mode 100644 test/context.spec.js 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/test/ContextTest.js b/test/ContextTest.js deleted file mode 100644 index e69de29..0000000 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()); + }); +}); From 6bb7d7e7d3207d66330b044e62bb20d50007c0ad Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 15:14:21 +0100 Subject: [PATCH 17/19] Add a few utils tests --- test/components/Authenticated.spec.js | 2 +- test/utils.spec.js | 75 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/utils.spec.js diff --git a/test/components/Authenticated.spec.js b/test/components/Authenticated.spec.js index 1d7bc07..9a96f91 100644 --- a/test/components/Authenticated.spec.js +++ b/test/components/Authenticated.spec.js @@ -1,5 +1,5 @@ import React from 'react'; -import {mount, shallow} from 'enzyme'; +import {shallow} from 'enzyme'; import {expect} from 'chai'; import Authenticated from '../../src/components/Authenticated'; 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; + }); + }); +}); From e208c9dba835da04bfea77de2a339f7a0c975b1b Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 15:22:52 +0100 Subject: [PATCH 18/19] Add babel-register as a dep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 56be4e3..ec875a4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.16.0", + "babel-register": "^6.23.0", "bundle-loader": "^0.5.4", "chai": "^3.5.0", "chai-spies": "^0.7.1", From 6c33bfd95fa9a6afa5a79a4b60bde6732071e433 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Wed, 15 Feb 2017 15:37:46 +0100 Subject: [PATCH 19/19] Fix build issues Lower minimum coverage to avoid failures. Change lib used to set ENV to allow building on older Node versions. --- package.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ec875a4..76a2090 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,17 @@ "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", - "test": "cross-env NODE_ENV=test nyc _mocha --report html -- test/helpers/browser.js test/*.spec.js test/**/*.spec.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", "babel-core": "^6.18.2", @@ -43,10 +51,10 @@ "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", - "cross-env": "^3.1.4", "css-loader": "^0.25.0", "enzyme": "^2.7.1", "eslint": "^3.9.1", @@ -94,6 +102,10 @@ "xtend": "^4.0.1" }, "nyc": { + "lines": "20", + "statements": "20", + "branches": "20", + "functions": "20", "sourceMap": false, "instrument": false, "include": "src/**/*.js",