From 6d119ba5a6c6fe1feb6a01023c48de069fba1b57 Mon Sep 17 00:00:00 2001 From: Diogo Beda Date: Sat, 16 Jul 2016 01:36:37 -0300 Subject: [PATCH 1/4] Add specs for sessions reducer --- app/lib/reducers/sessions.js | 8 +- app/package.json | 24 +++- app/spec/reducers/sessions.spec.js | 171 +++++++++++++++++++++++++++++ app/spec/support/helper.js | 11 ++ app/spec/support/mocks/electron.js | 19 ++++ app/spec/support/mocks/window.js | 7 ++ 6 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 app/spec/reducers/sessions.spec.js create mode 100644 app/spec/support/helper.js create mode 100644 app/spec/support/mocks/electron.js create mode 100644 app/spec/support/mocks/window.js diff --git a/app/lib/reducers/sessions.js b/app/lib/reducers/sessions.js index 84ad585a64bc..eabc230822d3 100644 --- a/app/lib/reducers/sessions.js +++ b/app/lib/reducers/sessions.js @@ -13,13 +13,13 @@ import { SESSION_SET_PROCESS_TITLE } from '../constants/sessions'; -const initialState = Immutable({ +export const initialState = Immutable({ sessions: {}, write: null, activeUid: null }); -function Session (obj) { +export function Session (obj) { return Immutable({ uid: '', title: '', @@ -31,14 +31,14 @@ function Session (obj) { }).merge(obj); } -function Write (obj) { +export function Write (obj) { return Immutable({ uid: '', data: '' }).merge(obj); } -const reducer = (state = initialState, action) => { +export const reducer = (state = initialState, action) => { switch (action.type) { case SESSION_ADD: return state.setIn(['sessions', action.uid], Session({ diff --git a/app/package.json b/app/package.json index 2da51a1d959b..ec0f3e568544 100644 --- a/app/package.json +++ b/app/package.json @@ -22,10 +22,13 @@ "seamless-immutable": "6.1.1" }, "devDependencies": { + "ava": "^0.15.2", + "ava-spec": "^1.0.1", "babel-cli": "6.10.1", "babel-core": "6.10.4", "babel-eslint": "6.1.1", "babel-loader": "6.2.4", + "babel-plugin-transform-es2015-modules-commonjs": "^6.10.3", "babel-preset-es2015-native-modules": "6.6.0", "babel-preset-react": "6.11.1", "eslint": "3.0.1", @@ -33,6 +36,7 @@ "eslint-plugin-promise": "1.3.2", "eslint-plugin-react": "5.2.2", "eslint-plugin-standard": "1.3.2", + "mockery": "^1.7.0", "webpack": "2.1.0-beta.15" }, "eslintConfig": { @@ -65,14 +69,32 @@ } }, "babel": { + "env": { + "test": { + "plugins": [ + "transform-es2015-modules-commonjs" + ] + } + }, "presets": [ "es2015-native-modules", "react" ] }, + "ava": { + "babel": "inherit", + "files": [ + "./spec/**/*.spec.js" + ], + "require": [ + "babel-register", + "./spec/support/helper.js" + ] + }, "scripts": { "dev": "webpack --watch", "lint": "eslint *.js", - "build": "NODE_ENV=production webpack" + "build": "NODE_ENV=production webpack", + "test": "NODE_ENV=test ava" } } diff --git a/app/spec/reducers/sessions.spec.js b/app/spec/reducers/sessions.spec.js new file mode 100644 index 000000000000..b841fce30d00 --- /dev/null +++ b/app/spec/reducers/sessions.spec.js @@ -0,0 +1,171 @@ +import { describe } from 'ava-spec'; +import { + reducer, + initialState, + Session, + Write +} from '../../lib/reducers/sessions'; +import { + SESSION_ADD, + SESSION_PTY_EXIT, + SESSION_USER_EXIT, + SESSION_PTY_DATA, + SESSION_SET_ACTIVE, + SESSION_CLEAR_ACTIVE, + SESSION_URL_SET, + SESSION_URL_UNSET, + SESSION_SET_XTERM_TITLE, + SESSION_SET_PROCESS_TITLE +} from '../../lib/constants/sessions'; + +describe('Sessions reducer', () => { + describe('on @@redux/INIT', it => { + const state = undefined; + const action = { type: '@@redux/INIT' }; + + it('should return initial state', t => { + t.deepEqual(reducer(state, action), initialState); + }); + }); + + describe('on SESSION_ADD', it => { + const state = initialState; + const action = { type: SESSION_ADD, uid: 'mysession', shell: '/my/shell' }; + + it('should add a session to the state based on its uid', t => { + const result = reducer(state, action); + + t.deepEqual(result.sessions, { + mysession: { + uid: 'mysession', title: '', write: null, url: null, cleared: false, shell: 'shell' + } + }); + }); + }); + + describe('on SESSION_URL_SET', it => { + const uid = 'mysession'; + const url = '/my/url'; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_URL_SET, uid, url }; + + it('should set the url on the action uid\'s session', t => { + const result = reducer(state, action); + + t.is(result.sessions.mysession.url, url); + }); + }); + + describe('on SESSION_URL_UNSET', it => { + const uid = 'mysession'; + const url = '/my/url'; + const state = initialState.setIn(['sessions', uid], Session({ uid, url })); + const action = { type: SESSION_URL_UNSET, uid }; + + it('should assign url as null on the action uid\'s session', t => { + const result = reducer(state, action); + + t.is(result.sessions.mysession.url, null); + }); + }); + + describe('on SESSION_SET_ACTIVE', it => { + const uid = 'mysession'; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_SET_ACTIVE, uid }; + + it('should set activeUid on session based on action uid', t => { + const result = reducer(state, action); + + t.is(result.activeUid, uid); + }); + }); + + describe('on SESSION_CLEAR_ACTIVE', it => { + const uid = 'mysession'; + const state = initialState + .setIn(['sessions', uid], Session({ uid })) + .setIn(['activeUid'], uid); + const action = { type: SESSION_CLEAR_ACTIVE }; + + it('should set "cleared: true" on state\'s active session', t => { + const result = reducer(state, action); + + t.true(result.sessions.mysession.cleared); + }); + }); + + describe('on SESSION_PTY_DATA', it => { + const uid = 'mysession'; + const data = { a: 'b' }; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_PTY_DATA, uid, data }; + + it('should set write on state based on action data', t => { + const result = reducer(state, action); + + t.deepEqual(result.write, Write(action)); + t.false(result.sessions.mysession.cleared); + }); + }); + + describe('on SESSION_PTY_EXIT', () => { + describe('when session is on state', it => { + const uid = 'mysession'; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_PTY_EXIT, uid }; + + it('should remove session from state', t => { + const result = reducer(state, action); + + t.is(result.sessions.mysession, undefined); + }); + }); + + describe('when session is not on state', it => { + const uid = 'mysession'; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_PTY_EXIT, uid: 'anotherUid' }; + + it('should return current state', t => { + t.deepEqual(reducer(state, action), state); + }); + }); + }); + + describe('on SESSION_USER_EXIT', it => { + const uid = 'mysession'; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_USER_EXIT, uid }; + + it('should remove session from state', t => { + const result = reducer(state, action); + + t.is(result.sessions.mysession, undefined); + }); + }); + + describe('on SESSION_SET_XTERM_TITLE', it => { + const uid = 'mysession'; + const title = 'mytitle'; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_SET_XTERM_TITLE, uid, title }; + + it('should set title on session based on action uid', t => { + const result = reducer(state, action); + t.is(result.sessions.mysession.title, title); + }); + }); + + describe('on SESSION_SET_PROCESS_TITLE', it => { + const uid = 'mysession'; + const title = 'mytitle'; + const state = initialState.setIn(['sessions', uid], Session({ uid })); + const action = { type: SESSION_SET_PROCESS_TITLE, uid, title }; + + it('should set title on session based on action uid', t => { + const result = reducer(state, action); + t.is(result.sessions.mysession.title, title); + }); + }); +}); \ No newline at end of file diff --git a/app/spec/support/helper.js b/app/spec/support/helper.js new file mode 100644 index 000000000000..c7e91176c799 --- /dev/null +++ b/app/spec/support/helper.js @@ -0,0 +1,11 @@ +import mockery from 'mockery'; +import electronMock from './mocks/electron'; +import windowMock from './mocks/window'; + +mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false +}); + +mockery.registerMock('electron', electronMock); +global.window = windowMock; \ No newline at end of file diff --git a/app/spec/support/mocks/electron.js b/app/spec/support/mocks/electron.js new file mode 100644 index 000000000000..bb54fc06804e --- /dev/null +++ b/app/spec/support/mocks/electron.js @@ -0,0 +1,19 @@ +const paths = { + plugins: { + concat: () => ['my/path', 'my/local/path'] + } +}; + +const plugins = { + getBasePaths: () => ({ + path: '/my/path', + localPath: '/my/local/path' + }), + getPaths: () => paths +}; + +export default { + remote: { + require: () => plugins + } +}; \ No newline at end of file diff --git a/app/spec/support/mocks/window.js b/app/spec/support/mocks/window.js new file mode 100644 index 000000000000..1cb94b295698 --- /dev/null +++ b/app/spec/support/mocks/window.js @@ -0,0 +1,7 @@ +const require = { + basename: () => 'basename' +}; + +export default { + require: () => require +}; \ No newline at end of file From 43c962f55701d6eac63f5bda515227ad6e420eeb Mon Sep 17 00:00:00 2001 From: Diogo Beda Date: Mon, 18 Jul 2016 00:50:50 -0300 Subject: [PATCH 2/4] Add ui reducer specs --- app/lib/reducers/ui.js | 4 +- app/spec/reducers/sessions.spec.js | 6 +- app/spec/reducers/ui.spec.js | 238 +++++++++++++++++++++++++++++ app/spec/support/helper.js | 2 +- app/spec/support/mocks/electron.js | 2 +- app/spec/support/mocks/window.js | 2 +- 6 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 app/spec/reducers/ui.spec.js diff --git a/app/lib/reducers/ui.js b/app/lib/reducers/ui.js index 05b873233673..9c23afc19994 100644 --- a/app/lib/reducers/ui.js +++ b/app/lib/reducers/ui.js @@ -13,7 +13,7 @@ import { import { UPDATE_AVAILABLE } from '../constants/updater'; // TODO: populate `config-default.js` from this :) -const initial = Immutable({ +export const initialState = Immutable({ cols: null, rows: null, activeUid: null, @@ -57,7 +57,7 @@ const initial = Immutable({ updateNotes: null }); -const reducer = (state = initial, action) => { +export const reducer = (state = initialState, action) => { let state_ = state; switch (action.type) { diff --git a/app/spec/reducers/sessions.spec.js b/app/spec/reducers/sessions.spec.js index b841fce30d00..a445ace9d808 100644 --- a/app/spec/reducers/sessions.spec.js +++ b/app/spec/reducers/sessions.spec.js @@ -30,14 +30,14 @@ describe('Sessions reducer', () => { describe('on SESSION_ADD', it => { const state = initialState; - const action = { type: SESSION_ADD, uid: 'mysession', shell: '/my/shell' }; + const action = { type: SESSION_ADD, uid: 'mysession', pid: 1, shell: '/my/shell' }; it('should add a session to the state based on its uid', t => { const result = reducer(state, action); t.deepEqual(result.sessions, { mysession: { - uid: 'mysession', title: '', write: null, url: null, cleared: false, shell: 'shell' + uid: 'mysession', title: '', write: null, url: null, cleared: false, shell: 'shell', pid: 1 } }); }); @@ -168,4 +168,4 @@ describe('Sessions reducer', () => { t.is(result.sessions.mysession.title, title); }); }); -}); \ No newline at end of file +}); diff --git a/app/spec/reducers/ui.spec.js b/app/spec/reducers/ui.spec.js new file mode 100644 index 000000000000..86cde7717dee --- /dev/null +++ b/app/spec/reducers/ui.spec.js @@ -0,0 +1,238 @@ +import { describe } from 'ava-spec'; +import { reducer, initialState } from '../../lib/reducers/ui'; +import { CONFIG_LOAD, CONFIG_RELOAD } from '../../lib/constants/config'; +import { UI_FONT_SIZE_SET, UI_FONT_SIZE_RESET } from '../../lib/constants/ui'; +import { NOTIFICATION_DISMISS } from '../../lib/constants/notifications'; +import { + SESSION_ADD, + SESSION_RESIZE, + SESSION_PTY_DATA, + SESSION_PTY_EXIT, + SESSION_SET_ACTIVE +} from '../../lib/constants/sessions'; +import { UPDATE_AVAILABLE } from '../../lib/constants/updater'; + +describe('UI reducer', () => { + describe('on @@redux/INIT', it => { + const state = undefined; + const action = { type: '@@redux/INIT' }; + + it('should return initial state', t => { + t.deepEqual(reducer(state, action), initialState); + }); + }); + + describe('on CONFIG_LOAD', it => { + const config = { cursorColor: '#f2f2f2' }; + const state = initialState; + const action = { type: CONFIG_LOAD, config }; + + it('should return the state with updated properties', t => { + t.deepEqual(reducer(state, action), state.merge(config)); + }); + }); + + describe('on CONFIG_RELOAD', it => { + const config = { cursorColor: '#f2f2f2' }; + const state = initialState; + const action = { type: CONFIG_RELOAD, config }; + + it('should return the state with updated properties', t => { + t.deepEqual(reducer(state, action), state.merge(config)); + }); + }); + + describe('on SESSION_ADD', it => { + const uid = 'mysession'; + const state = initialState; + const action = { type: SESSION_ADD, uid }; + + it('should update timestamp on openAt based on action uid', t => { + const result = reducer(state, action); + t.truthy(result.openAt.mysession); + }); + }); + + describe('on SESSION_RESIZE', it => { + const size = 10; + const state = initialState; + const action = { type: SESSION_RESIZE, rows: size, cols: size }; + + it('should update rows', t => { + const result = reducer(state, action); + t.is(result.rows, size); + }); + + it('should update cols', t => { + const result = reducer(state, action); + t.is(result.cols, size); + }); + + it('should update resizeAt', t => { + const result = reducer(state, action); + t.truthy(result.resizeAt); + }); + }); + + describe('on SESSION_PTY_EXIT', it => { + const uid = 'mysession'; + const state = initialState + .setIn(['openAt', uid], Date.now()) + .setIn(['activityMarkers', uid], false); + const action = { type: SESSION_PTY_EXIT, uid }; + + it('should remove openAt key corresponding to action uid', t => { + const result = reducer(state, action); + t.is(result.openAt.mysession, undefined); + }); + + it('should remove activityMarkers key corresponding to action uid', t => { + const result = reducer(state, action); + t.is(result.activityMarkers.mysession, undefined); + }); + }); + + describe('on SESSION_SET_ACTIVE', it => { + const uid = 'mysession'; + const state = initialState + .setIn(['activityMarkers', uid], true); + const action = { type: SESSION_SET_ACTIVE, uid }; + + it('should set activeUid based on action uid', t => { + const result = reducer(state, action); + t.is(result.activeUid, uid); + }); + + it('should set action uid key to false on activityMarkers', t => { + const result = reducer(state, action); + t.false(result.activityMarkers.mysession); + }); + }); + + describe('on SESSION_PTY_DATA', () => { + describe('when action uid is equal to activeUid', it => { + const uid = 'mysession'; + const state = initialState.set('activeUid', uid); + const action = { type: SESSION_PTY_DATA, uid }; + + it('should not update state', t => { + t.deepEqual(reducer(state, action), state); + }); + }); + + describe('when action time is at least 1000ms after session open', it => { + const uid = 'mysession'; + const state = initialState.setIn(['openAt', uid], Date.now()); + const action = { type: SESSION_PTY_DATA, uid }; + + it('should not update state', t => { + t.deepEqual(reducer(state, action), state); + }); + }); + + describe('when action time is at least 1000ms after a resize event', it => { + const uid = 'mysession'; + const state = initialState.set('resizeAt', Date.now()); + const action = { type: SESSION_PTY_DATA, uid }; + + it('should not update state', t => { + t.deepEqual(reducer(state, action), state); + }); + }); + + describe('when action time is not too close to a resize event', it => { + const uid = 'mysession'; + const state = initialState; + const action = { type: SESSION_PTY_DATA, uid }; + + it('should set action uid key to true on activityMarkers', t => { + const result = reducer(state, action); + t.true(result.activityMarkers.mysession); + }); + }); + }); + + describe('on UI_FONT_SIZE_SET', it => { + const value = 14; + const state = initialState; + const action = { type: UI_FONT_SIZE_SET, value }; + + it('should assign action.value to fontSizeOverride', t => { + const result = reducer(state, action); + t.is(result.fontSizeOverride, value); + }); + }); + + describe('on UI_FONT_SIZE_RESET', it => { + const state = initialState; + const action = { type: UI_FONT_SIZE_RESET }; + + it('should assign null to fontSizeOverride', t => { + const result = reducer(state, action); + t.is(result.fontSizeOverride, null); + }); + }); + + describe('on NOTIFICATION_DISMISS', it => { + const id = 'mynotification'; + const state = initialState.setIn(['notifications', id], true); + const action = { type: NOTIFICATION_DISMISS, id }; + + it('should set action uid key to false on notifications', t => { + const result = reducer(state, action); + t.false(result.notifications.mynotification); + }); + }); + + describe('on UPDATE_AVAILABLE', it => { + const version = '1.0.0'; + const notes = 'awesome note'; + const state = initialState; + const action = { type: UPDATE_AVAILABLE, version, notes }; + + it('should assign action.version to updateVersion', t => { + const result = reducer(state, action); + t.is(result.updateVersion, version); + }); + + it('should assign action.notes to updateNotes', t => { + const result = reducer(state, action); + t.is(result.updateNotes, notes); + }); + }); + + describe('on any action type', () => { + describe('when rows or cols have been changed', it => { + const size = 10; + const state = initialState.set('rows', 1).set('cols', 1); + const action = { type: SESSION_RESIZE, rows: size, cols: size }; + + it('should set resize key to true on notifications', t => { + const result = reducer(state, action); + t.true(result.notifications.resize); + }); + }); + + describe('when updateVersion have been changed', it => { + const version = '1.0.0'; + const state = initialState; + const action = { type: UPDATE_AVAILABLE, version }; + + it('should set updates key to true on notifications', t => { + const result = reducer(state, action); + t.true(result.notifications.updates); + }); + }); + + describe('when any of the font size values change and action type is not CONFIG_LOAD', it => { + const config = { fontSize: 14 }; + const state = initialState; + const action = { type: CONFIG_RELOAD, config }; + + it('should set font key to true on notifications', t => { + const result = reducer(state, action); + t.true(result.notifications.font); + }); + }); + }); +}); diff --git a/app/spec/support/helper.js b/app/spec/support/helper.js index c7e91176c799..c0f3d02cebdb 100644 --- a/app/spec/support/helper.js +++ b/app/spec/support/helper.js @@ -8,4 +8,4 @@ mockery.enable({ }); mockery.registerMock('electron', electronMock); -global.window = windowMock; \ No newline at end of file +global.window = windowMock; diff --git a/app/spec/support/mocks/electron.js b/app/spec/support/mocks/electron.js index bb54fc06804e..53132cea89ce 100644 --- a/app/spec/support/mocks/electron.js +++ b/app/spec/support/mocks/electron.js @@ -16,4 +16,4 @@ export default { remote: { require: () => plugins } -}; \ No newline at end of file +}; diff --git a/app/spec/support/mocks/window.js b/app/spec/support/mocks/window.js index 1cb94b295698..f5147e713b3a 100644 --- a/app/spec/support/mocks/window.js +++ b/app/spec/support/mocks/window.js @@ -4,4 +4,4 @@ const require = { export default { require: () => require -}; \ No newline at end of file +}; From 628fb396d7f67a9a27bb741f606b6b5bc75ab821 Mon Sep 17 00:00:00 2001 From: Diogo Beda Date: Mon, 18 Jul 2016 01:11:38 -0300 Subject: [PATCH 3/4] Add coverage config and test to travis file --- .gitignore | 4 ++++ .travis.yml | 1 + app/package.json | 8 +++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 15daa358be37..8289a45fbd92 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ build # dependencies node_modules +# coverage +coverage +.nyc_output + # logs npm-debug.log diff --git a/.travis.yml b/.travis.yml index 0f4c51f47d5f..dd81cfaa13fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,4 +36,5 @@ before_script: - sleep 3 script: + - npm test - npm run lint diff --git a/app/package.json b/app/package.json index ec0f3e568544..a2e890b2b9b2 100644 --- a/app/package.json +++ b/app/package.json @@ -37,6 +37,7 @@ "eslint-plugin-react": "5.2.2", "eslint-plugin-standard": "1.3.2", "mockery": "^1.7.0", + "nyc": "^7.0.0", "webpack": "2.1.0-beta.15" }, "eslintConfig": { @@ -91,10 +92,15 @@ "./spec/support/helper.js" ] }, + "nyc": { + "include": ["lib/**/*.js"], + "reporter": ["lcov", "text-summary"] + }, "scripts": { "dev": "webpack --watch", "lint": "eslint *.js", "build": "NODE_ENV=production webpack", - "test": "NODE_ENV=test ava" + "test": "NODE_ENV=test ava", + "coverage": "NODE_ENV=test nyc ava" } } From f071c92de5a98a89954ed13fad1d47991e62b38a Mon Sep 17 00:00:00 2001 From: Diogo Beda Date: Mon, 18 Jul 2016 22:18:07 -0300 Subject: [PATCH 4/4] Pin test dependencies --- app/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/package.json b/app/package.json index a2e890b2b9b2..9121129fa34b 100644 --- a/app/package.json +++ b/app/package.json @@ -22,13 +22,13 @@ "seamless-immutable": "6.1.1" }, "devDependencies": { - "ava": "^0.15.2", - "ava-spec": "^1.0.1", + "ava": "0.15.2", + "ava-spec": "1.0.1", "babel-cli": "6.10.1", "babel-core": "6.10.4", "babel-eslint": "6.1.1", "babel-loader": "6.2.4", - "babel-plugin-transform-es2015-modules-commonjs": "^6.10.3", + "babel-plugin-transform-es2015-modules-commonjs": "6.10.3", "babel-preset-es2015-native-modules": "6.6.0", "babel-preset-react": "6.11.1", "eslint": "3.0.1", @@ -36,8 +36,8 @@ "eslint-plugin-promise": "1.3.2", "eslint-plugin-react": "5.2.2", "eslint-plugin-standard": "1.3.2", - "mockery": "^1.7.0", - "nyc": "^7.0.0", + "mockery": "1.7.0", + "nyc": "7.0.0", "webpack": "2.1.0-beta.15" }, "eslintConfig": {