diff --git a/16 Immutablejs/karma.conf.js b/16 Immutablejs/karma.conf.js new file mode 100644 index 0000000..e76c696 --- /dev/null +++ b/16 Immutablejs/karma.conf.js @@ -0,0 +1,56 @@ +var webpackConfig = require('./webpack.config'); + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['mocha', 'chai', 'sinon'], + files: [ + './test/test_index.js' + ], + exclude: [ + ], + preprocessors: { + './test/test_index.js': ['webpack', 'sourcemap'] + }, + webpack: { + devtool: 'inline-source-map', + module: { + loaders: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + loader: 'ts-loader' + }, + //Configuration required by enzyme + { + test: /\.json$/, + loader: 'json' + } + ] + }, + resolve: { + //Added .json extension required by cheerio (enzyme dependency) + extensions: ['', '.js', '.ts', '.tsx', '.json'] + }, + //Configuration required by enzyme + externals: { + 'react/lib/ExecutionEnvironment': true, + 'react/lib/ReactContext': 'window', + } + }, + webpackMiddleware: { + // webpack-dev-middleware configuration + // i. e. + noInfo: true + }, + + reporters: ['progress'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + concurrency: Infinity + }) +} diff --git a/16 Immutablejs/package.json b/16 Immutablejs/package.json new file mode 100644 index 0000000..bbfc3db --- /dev/null +++ b/16 Immutablejs/package.json @@ -0,0 +1,54 @@ +{ + "name": "reactboiler", + "version": "1.0.0", + "description": "", + "scripts": { + "postinstall": "tsd reinstall --overwrite --save", + "start": "webpack-dev-server", + "test": "karma start" + }, + "author": "Braulio Diez", + "license": "ISC", + "dependencies": { + "bootstrap": "^3.3.5", + "immutable": "^3.8.1", + "jquery": "^2.1.4", + "lodash": "^4.5.1", + "object-assign": "^4.0.1", + "q": "^1.4.1", + "react": "~0.14.7", + "react-dom": "^0.14.7", + "react-redux": "^4.4.0", + "react-router": "^2.0.0", + "redux": "^3.3.1", + "redux-thunk": "^2.0.1", + "toastr": "^2.1.2" + }, + "devDependencies": { + "chai": "^3.5.0", + "css-loader": "^0.23.1", + "deep-freeze": "0.0.1", + "enzyme": "^2.1.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.8.5", + "html-webpack-plugin": "^2.9.0", + "json-loader": "^0.5.4", + "karma": "^0.13.22", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^0.2.3", + "karma-mocha": "^0.2.2", + "karma-sinon": "^1.0.4", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^1.7.0", + "mocha": "^2.4.5", + "react-addons-test-utils": "^0.14.7", + "redux-mock-store": "^1.0.2", + "sinon": "^1.17.3", + "style-loader": "^0.13.0", + "ts-loader": "^0.8.1", + "typescript": "^1.8.9", + "url-loader": "^0.5.7", + "webpack": "^1.12.13", + "webpack-dev-server": "~1.10.1" + } +} diff --git a/16 Immutablejs/src/actions/assignMembers.ts b/16 Immutablejs/src/actions/assignMembers.ts new file mode 100644 index 0000000..a69533e --- /dev/null +++ b/16 Immutablejs/src/actions/assignMembers.ts @@ -0,0 +1,11 @@ +import MemberEntity from '../api/memberEntity'; + + +const assignMembers = (members : any) => { + return { + type: 'MEMBERS_ASSIGN' + ,members: members + } + } + +export default assignMembers; diff --git a/16 Immutablejs/src/actions/assignRepos.ts b/16 Immutablejs/src/actions/assignRepos.ts new file mode 100644 index 0000000..d63a3bd --- /dev/null +++ b/16 Immutablejs/src/actions/assignRepos.ts @@ -0,0 +1,10 @@ +import RepoEntity from '../api/repoEntity'; + +const assignRepos = (repos: Array) => { + return { + type: 'REPOS_ASSIGN', + repos: repos + }; +}; + +export default assignRepos; diff --git a/16 Immutablejs/src/actions/httpCallCompleted.ts b/16 Immutablejs/src/actions/httpCallCompleted.ts new file mode 100644 index 0000000..5fe9701 --- /dev/null +++ b/16 Immutablejs/src/actions/httpCallCompleted.ts @@ -0,0 +1,7 @@ +const httpCallCompleted = () => { + return { + type: 'HTTP_GET_CALL_COMPLETED' + } +} + +export default httpCallCompleted; diff --git a/16 Immutablejs/src/actions/httpCallStarted.ts b/16 Immutablejs/src/actions/httpCallStarted.ts new file mode 100644 index 0000000..1c96957 --- /dev/null +++ b/16 Immutablejs/src/actions/httpCallStarted.ts @@ -0,0 +1,7 @@ +const httpCallStarted = () => { + return { + type: 'HTTP_GET_CALL_STARTED' + } +} + +export default httpCallStarted; diff --git a/16 Immutablejs/src/actions/httpInitializeDispatcher.ts b/16 Immutablejs/src/actions/httpInitializeDispatcher.ts new file mode 100644 index 0000000..449fc44 --- /dev/null +++ b/16 Immutablejs/src/actions/httpInitializeDispatcher.ts @@ -0,0 +1,13 @@ +import http from '../http/http'; + +const httpInitializeDispatcher = (dispatcher) => { + http.Initialize(dispatcher); + + return { + type: 'HTTP_INITIALIZE_DISPATCHER' + } +} + +export { + httpInitializeDispatcher +} diff --git a/16 Immutablejs/src/actions/initializeNewMember.ts b/16 Immutablejs/src/actions/initializeNewMember.ts new file mode 100644 index 0000000..41f66e1 --- /dev/null +++ b/16 Immutablejs/src/actions/initializeNewMember.ts @@ -0,0 +1,9 @@ +const initializeNewMember = () => { + return { + type: 'MEMBER_INITIALIZE_NEW' + } +} + +export { + initializeNewMember +}; diff --git a/16 Immutablejs/src/actions/loadMember.ts b/16 Immutablejs/src/actions/loadMember.ts new file mode 100644 index 0000000..c60151f --- /dev/null +++ b/16 Immutablejs/src/actions/loadMember.ts @@ -0,0 +1,12 @@ +import memberAPI from '../api/memberAPI'; + +const loadMember = (id : number) => { + return { + type: 'MEMBER_LOAD' + ,member: memberAPI.getMemberById(id) + } +} + +export { + loadMember +}; diff --git a/16 Immutablejs/src/actions/loadMembers.ts b/16 Immutablejs/src/actions/loadMembers.ts new file mode 100644 index 0000000..cf72281 --- /dev/null +++ b/16 Immutablejs/src/actions/loadMembers.ts @@ -0,0 +1,27 @@ +import MemberEntity from '../api/memberEntity'; +import MemberAPI from '../api/memberAPI'; +import assignMembers from './assignMembers'; +import * as Q from 'q'; + +function loadMembers() { + + // Invert control! + // Return a function that accepts `dispatch` so we can dispatch later. + // Thunk middleware knows how to turn thunk async actions into actions. + + return dispatcher => { + var promise : Q.Promise; + + promise = MemberAPI.getAllMembersAsync(); + + promise.then( + data => dispatcher(assignMembers(data)) + ); + + return promise; + }; +} + +export { + loadMembers +}; diff --git a/16 Immutablejs/src/actions/loadRepos.ts b/16 Immutablejs/src/actions/loadRepos.ts new file mode 100644 index 0000000..c3e9772 --- /dev/null +++ b/16 Immutablejs/src/actions/loadRepos.ts @@ -0,0 +1,20 @@ +import RepoEntity from '../api/repoEntity'; +import RepoAPI from '../api/repoAPI'; +import assignRepos from './assignRepos'; + +function loadRepos() { + return dispatcher => { + var promise: Q.Promise; + promise = RepoAPI.getAllReposAsync(); + + promise.then( + data => dispatcher(assignRepos(data)) + ); + + return promise; + } +} + +export { + loadRepos +}; diff --git a/16 Immutablejs/src/actions/resetSaveCompleted.ts b/16 Immutablejs/src/actions/resetSaveCompleted.ts new file mode 100644 index 0000000..af94c35 --- /dev/null +++ b/16 Immutablejs/src/actions/resetSaveCompleted.ts @@ -0,0 +1,10 @@ + +const resetSaveCompleted = () => { + return { + type: 'MEMBER_RESET_SAVE_COMPLETED' + } +} + +export { + resetSaveCompleted +}; diff --git a/16 Immutablejs/src/actions/saveMember.ts b/16 Immutablejs/src/actions/saveMember.ts new file mode 100644 index 0000000..354d217 --- /dev/null +++ b/16 Immutablejs/src/actions/saveMember.ts @@ -0,0 +1,25 @@ +import MemberEntity from "../api/MemberEntity"; +import memberAPI from '../api/memberAPI'; +import MemberFormErrors from '../validations/memberFormErrors'; +import MemberFormValidator from '../validations/memberFormValidator'; + +const saveMember = (member : MemberEntity) => { + // Candidate to be splitted + let errorsSave : MemberFormErrors = MemberFormValidator.validateMember(member); + + if(errorsSave.isEntityValid) { + // Since this is using fake api this method is synchronous + // if you are looking for a sample that handles and async request + // take look to the action file LoadMembers.ts + memberAPI.saveAuthor(member); + } + + return { + type: 'MEMBER_SAVE' + ,errors : errorsSave + } +} + +export { + saveMember +}; diff --git a/16 Immutablejs/src/actions/spec/assignMembers.spec.ts b/16 Immutablejs/src/actions/spec/assignMembers.spec.ts new file mode 100644 index 0000000..66ad001 --- /dev/null +++ b/16 Immutablejs/src/actions/spec/assignMembers.spec.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai'; +import assignMembers from '../assignMembers'; +import MemberEntity from '../../api/memberEntity'; + +describe('assignMembers', () => { + it('should return members action type: MEMBERS_ASSIGN and members: [] when passing an empty members Array', () => { + // Arrange + const members = new Array(); + + // Act + let result = assignMembers(members); + + // Assert + expect(result.type).to.be.equal('MEMBERS_ASSIGN'); + expect(result.members.length).to.be.equal(0); + }); + + it('should return members action type: MEMBERS_ASSIGN and members: member Array with two items when passing ' + + 'a member Array with two items', () => { + // Arrange + const members = new Array(); + + let member1 = new MemberEntity(); + let member2 = new MemberEntity(); + + member1.login = 'test1'; + member2.login = 'test2'; + + members.push(member1); + members.push(member2); + + // Act + let result = assignMembers(members); + + // Assert + expect(result.type).to.be.equal('MEMBERS_ASSIGN'); + expect(result.members.length).to.be.equal(2); + expect(result.members[0].login).to.be.equal('test1'); + expect(result.members[1].login).to.be.equal('test2'); + }); +}); diff --git a/16 Immutablejs/src/actions/spec/assignRepos.spec.ts b/16 Immutablejs/src/actions/spec/assignRepos.spec.ts new file mode 100644 index 0000000..d9e6505 --- /dev/null +++ b/16 Immutablejs/src/actions/spec/assignRepos.spec.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai'; +import assignRepos from '../assignRepos'; +import RepoEntity from '../../api/repoEntity'; + +describe('assignRepos', () => { + it('should return repos action type: REPOS_ASSIGN and repos: [] when passing an empty repos Array', () => { + // Arrange + const repos = new Array(); + + // Act + let result = assignRepos(repos); + + // Assert + expect(result.type).to.be.equal('REPOS_ASSIGN'); + expect(result.repos.length).to.be.equal(0); + }); + + it('should return repos action type: REPOS_ASSIGN and repos: repo Array with two items when passing ' + + 'a repo Array with two items', () => { + // Arrange + const repos = new Array(); + + let repo1 = new RepoEntity(); + let repo2 = new RepoEntity(); + + repo1.name = 'test1'; + repo2.name = 'test2'; + + repos.push(repo1); + repos.push(repo2); + + // Act + let result = assignRepos(repos); + + // Assert + expect(result.type).to.be.equal('REPOS_ASSIGN'); + expect(result.repos.length).to.be.equal(2); + expect(result.repos[0].name).to.be.equal('test1'); + expect(result.repos[1].name).to.be.equal('test2'); + }); +}); diff --git a/16 Immutablejs/src/actions/spec/httpCallCompleted.spec.ts b/16 Immutablejs/src/actions/spec/httpCallCompleted.spec.ts new file mode 100644 index 0000000..bca750b --- /dev/null +++ b/16 Immutablejs/src/actions/spec/httpCallCompleted.spec.ts @@ -0,0 +1,14 @@ +import { expect } from 'chai'; +import httpCallCompleted from '../httpCallCompleted'; + +describe('httpCallCompleted', () => { + it('should return http action type: HTTP_GET_CALL_COMPLETED when calling httpCallCompleted', () => { + // Arrange + + // Act + let result = httpCallCompleted(); + + // Assert + expect(result.type).to.be.equal('HTTP_GET_CALL_COMPLETED'); + }); +}) diff --git a/16 Immutablejs/src/actions/spec/httpCallStarted.spec.ts b/16 Immutablejs/src/actions/spec/httpCallStarted.spec.ts new file mode 100644 index 0000000..4eca211 --- /dev/null +++ b/16 Immutablejs/src/actions/spec/httpCallStarted.spec.ts @@ -0,0 +1,14 @@ +import { expect } from 'chai'; +import httpCallStarted from '../httpCallStarted'; + +describe('httpCallStarted', () => { + it('should return http action type: HTTP_GET_CALL_STARTED when calling httpCallStarted', () => { + // Arrange + + // Act + let result = httpCallStarted(); + + // Assert + expect(result.type).to.be.equal('HTTP_GET_CALL_STARTED'); + }); +}) diff --git a/16 Immutablejs/src/actions/spec/httpInitializeDispatcher.spec.ts b/16 Immutablejs/src/actions/spec/httpInitializeDispatcher.spec.ts new file mode 100644 index 0000000..426db8b --- /dev/null +++ b/16 Immutablejs/src/actions/spec/httpInitializeDispatcher.spec.ts @@ -0,0 +1,39 @@ +import { expect } from 'chai'; +import { httpInitializeDispatcher } from '../httpInitializeDispatcher'; +import http from '../../http/http'; + +describe('httpInitializeDispatcher', () => { + it('should return http action type: HTTP_INITIALIZE_DISPATCHER and calls to http.Initialize(dispatcher) method ' + + 'when passing dispatcher equals empty object', sinon.test(() => { + // Arrange + let sinon: Sinon.SinonStatic = this; + let dispatcher = {}; + + let httpInitializeMethodStub = sinon.stub(http, "Initialize"); + // Act + let result = httpInitializeDispatcher(dispatcher); + + // Assert + expect(result.type).to.be.equal('HTTP_INITIALIZE_DISPATCHER'); + expect(httpInitializeMethodStub.called).to.be.true; + expect(httpInitializeMethodStub.calledWith(dispatcher)).to.be.true; + }).bind(this)); + + it('should return http action type: HTTP_INITIALIZE_DISPATCHER and calls to http.Initialize(dispatcher) method ' + + 'when passing dispatcher equals { testField: "test" }', sinon.test(() => { + // Arrange + let sinon: Sinon.SinonStatic = this; + let dispatcher = { + testField: "test" + }; + + let httpInitializeMethodStub = sinon.stub(http, "Initialize"); + // Act + let result = httpInitializeDispatcher(dispatcher); + + // Assert + expect(result.type).to.be.equal('HTTP_INITIALIZE_DISPATCHER'); + expect(httpInitializeMethodStub.called).to.be.true; + expect(httpInitializeMethodStub.calledWith(dispatcher)).to.be.true; + }).bind(this)); +}) diff --git a/16 Immutablejs/src/actions/spec/initializeNewMember.spec.ts b/16 Immutablejs/src/actions/spec/initializeNewMember.spec.ts new file mode 100644 index 0000000..57ea86b --- /dev/null +++ b/16 Immutablejs/src/actions/spec/initializeNewMember.spec.ts @@ -0,0 +1,14 @@ +import { expect } from 'chai'; +import { initializeNewMember } from '../initializeNewMember'; + +describe('initializeNewMember', () => { + it('should return http action type: MEMBER_INITIALIZE_NEW when calling initializeNewMember', () => { + // Arrange + + // Act + let result = initializeNewMember(); + + // Assert + expect(result.type).to.be.equal('MEMBER_INITIALIZE_NEW'); + }); +}) diff --git a/16 Immutablejs/src/actions/spec/loadMember.spec.ts b/16 Immutablejs/src/actions/spec/loadMember.spec.ts new file mode 100644 index 0000000..5cc0f24 --- /dev/null +++ b/16 Immutablejs/src/actions/spec/loadMember.spec.ts @@ -0,0 +1,28 @@ +import { expect } from 'chai'; +import { loadMember } from '../loadMember'; +import memberAPI from '../../api/memberAPI'; +import MemberEntity from '../../api/memberEntity'; + +describe('loadMember', () => { + it('should return action equals {type: MEMBER_LOAD, member: member } and calls to memberAPI.getMemberById(id) method ' + + 'when passing id equals 1', sinon.test(() => { + // Arrange + let sinon: Sinon.SinonStatic = this; + let member = new MemberEntity(); + member.id = 1; + + let getMemberByIdMethodStub = sinon.stub(memberAPI, "getMemberById"); + getMemberByIdMethodStub.returns(member); + + let id = 1; + + // Act + let result = loadMember(id); + + // Assert + expect(result.type).to.be.equal('MEMBER_LOAD'); + expect(result.member.id).to.be.equal(id); + expect(getMemberByIdMethodStub.called).to.be.true; + expect(getMemberByIdMethodStub.calledWith(id)).to.be.true; + }).bind(this)); +}) diff --git a/16 Immutablejs/src/actions/spec/loadMembers.spec.ts b/16 Immutablejs/src/actions/spec/loadMembers.spec.ts new file mode 100644 index 0000000..051b4db --- /dev/null +++ b/16 Immutablejs/src/actions/spec/loadMembers.spec.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import configureStore = require('redux-mock-store'); +import { loadMembers } from '../loadMembers'; +import MemberEntity from '../../api/memberEntity'; +import memberAPI from '../../api/memberAPI'; +import ReduxThunk from 'redux-thunk'; + +const middlewares = [ ReduxThunk ]; +const mockStore = configureStore(middlewares); + +describe('loadMembers', () => { + it('should return a promise, and this promise dispatch assignMembers action that returns ' + + 'an action equals { type: MEMBERS_ASSIGN, members: expectedMembers }', sinon.test((done) => { + let sinon: Sinon.SinonStatic = this; + let member1 = new MemberEntity(); + let member2 = new MemberEntity(); + + member1.login = "test1"; + member2.login = "test2" + + let expectedMembers : Array = [member1, member2]; + + // Arrange + let getAllMembersAsyncMethodStub = sinon.stub(memberAPI, 'getAllMembersAsync'); + getAllMembersAsyncMethodStub.returns({ + then: callback => { + callback(expectedMembers); + } + }); + + const expectedAction = { + type: 'MEMBERS_ASSIGN', + members: expectedMembers + } + + // Act + const store = mockStore([]); + + store.dispatch(loadMembers()) + .then(() => { + expect(store.getActions()[0].type).to.be.equal((expectedAction.type)); + expect(store.getActions()[0].members.length).to.be.equal(2); + done(); + }); + }).bind(this)); +}); diff --git a/16 Immutablejs/src/actions/spec/loadRepos.spec.ts b/16 Immutablejs/src/actions/spec/loadRepos.spec.ts new file mode 100644 index 0000000..45b1186 --- /dev/null +++ b/16 Immutablejs/src/actions/spec/loadRepos.spec.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import configureStore = require('redux-mock-store'); +import { loadRepos } from '../loadRepos'; +import RepoEntity from '../../api/repoEntity'; +import repoAPI from '../../api/repoAPI'; +import ReduxThunk from 'redux-thunk'; + +const middlewares = [ ReduxThunk ]; +const mockStore = configureStore(middlewares); + +describe('loadRepos', () => { + it('should return a promise, and this promise dispatch assignRepos action that returns ' + + 'an action equals { type: REPOS_ASSIGN, repos: expectedRepos }', sinon.test((done) => { + let sinon : Sinon.SinonStatic = this; + let repo1 = new RepoEntity(); + let repo2 = new RepoEntity(); + + repo1.name = "test1"; + repo2.name = "test2" + + let expectedRepos : Array = [repo1, repo2]; + + // Arrange + let getAllReposAsyncMethodStub = sinon.stub(repoAPI, 'getAllReposAsync'); + getAllReposAsyncMethodStub.returns({ + then: callback => { + callback(expectedRepos); + } + }); + + const expectedAction = { + type: 'REPOS_ASSIGN', + repos: expectedRepos + } + + // Act + const store = mockStore([]); + + store.dispatch(loadRepos()) + .then(() => { + expect(store.getActions()[0].type).to.be.equal((expectedAction.type)); + expect(store.getActions()[0].repos.length).to.be.equal(2); + done(); + }); + }).bind(this)); +}); diff --git a/16 Immutablejs/src/actions/spec/resetSaveCompleted.spec.ts b/16 Immutablejs/src/actions/spec/resetSaveCompleted.spec.ts new file mode 100644 index 0000000..fde2c2e --- /dev/null +++ b/16 Immutablejs/src/actions/spec/resetSaveCompleted.spec.ts @@ -0,0 +1,14 @@ +import { expect } from 'chai'; +import { resetSaveCompleted } from '../resetSaveCompleted'; + +describe('resetSaveCompleted', () => { + it('should return an action equals { type: MEMBER_RESET_SAVE_COMPLETED } when calling resetSaveCompleted', () => { + // Arrange + + // Act + let result = resetSaveCompleted(); + + // Assert + expect(result.type).to.be.equal('MEMBER_RESET_SAVE_COMPLETED'); + }); +}) diff --git a/16 Immutablejs/src/actions/spec/saveMember.spec.ts b/16 Immutablejs/src/actions/spec/saveMember.spec.ts new file mode 100644 index 0000000..552f0cd --- /dev/null +++ b/16 Immutablejs/src/actions/spec/saveMember.spec.ts @@ -0,0 +1,57 @@ +import { expect } from 'chai'; +import { saveMember } from '../saveMember'; +import MemberEntity from '../../api/memberEntity'; +import memberAPI from '../../api/memberAPI'; +import MemberErrors from '../../validations/memberFormErrors' +import MemberFormValidator from '../../validations/memberFormValidator'; + + +describe('saveMember', () => { + it('should return an action equals { type: "MEMBER_SAVE", errors { isEntityValid: true } } and calls to ' + + 'validateMember and saveAuthor methods when passing valid member', sinon.test(() => { + // Arrange + let sinon : Sinon.SinonStatic = this; + let member = new MemberEntity(); + let mockedValidationError = new MemberErrors(); + mockedValidationError.isEntityValid = true; + + let validateMemberMethodStub = sinon.stub(MemberFormValidator, 'validateMember'); + validateMemberMethodStub.returns(mockedValidationError); + + let saveAuthorMethodStub = sinon.stub(memberAPI, 'saveAuthor'); + + // Act + let result = saveMember(member); + + // Assert + expect(result.type).to.be.equal('MEMBER_SAVE'); + expect(result.errors.isEntityValid).to.be.true; + expect(validateMemberMethodStub.called).to.be.true; + expect(saveAuthorMethodStub.called).to.be.true; + expect(saveAuthorMethodStub.calledWith(member)).to.be.true; + }).bind(this)); + + it('should return an action equals { type: "MEMBER_SAVE", errors { isEntityValid: false } } and calls to ' + + 'validateMember method when passing invalid member', sinon.test(() => { + // Arrange + let sinon : Sinon.SinonStatic = this; + let member = new MemberEntity(); + let mockedValidationError = new MemberErrors(); + mockedValidationError.isEntityValid = false; + + let validateMemberMethodStub = sinon.stub(MemberFormValidator, 'validateMember'); + validateMemberMethodStub.returns(mockedValidationError); + + let saveAuthorMethodStub = sinon.stub(memberAPI, 'saveAuthor'); + + // Act + let result = saveMember(member); + + // Assert + expect(result.type).to.be.equal('MEMBER_SAVE'); + expect(result.errors.isEntityValid).to.be.false; + expect(validateMemberMethodStub.called).to.be.true; + expect(saveAuthorMethodStub.called).to.be.false; + expect(saveAuthorMethodStub.calledWith(member)).to.be.false; + }).bind(this)); +}); diff --git a/16 Immutablejs/src/actions/spec/uiInputMember.spec.ts b/16 Immutablejs/src/actions/spec/uiInputMember.spec.ts new file mode 100644 index 0000000..14b11b1 --- /dev/null +++ b/16 Immutablejs/src/actions/spec/uiInputMember.spec.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import { uiInputMember } from '../uiInputMember'; + +describe('uiInputMember', () => { + it('should returns an action equals { type: "MEMBER_UI_INPUT", fieldName: undefined, value: undefined } ' + + 'when passing fieldName equals undefined and value equals undefined', () => { + //Arrange + let fieldName = undefined; + let value = undefined; + + //Act + let result = uiInputMember(fieldName, value); + + //Assert + expect(result.type).to.be.equals("MEMBER_UI_INPUT"); + expect(result.fieldName).to.be.undefined; + expect(result.value).to.be.undefined; + }); + + it('should returns an action equals { type: "MEMBER_UI_INPUT", fieldName: null, value: null } ' + + 'when passing fieldName equals null and value equals null', () => { + //Arrange + let fieldName = null; + let value = null; + + //Act + let result = uiInputMember(fieldName, value); + + //Assert + expect(result.type).to.be.equals("MEMBER_UI_INPUT"); + expect(result.fieldName).to.be.null; + expect(result.value).to.be.null; + }); + + it('should returns an action equals { type: "MEMBER_UI_INPUT", fieldName: "test", value: 1 } ' + + 'when passing fieldName equals "test" and value equals 1', () => { + //Arrange + let fieldName = "test"; + let value = 1; + + //Act + let result = uiInputMember(fieldName, value); + + //Assert + expect(result.type).to.be.equals("MEMBER_UI_INPUT"); + expect(result.fieldName).to.be.equals("test"); + expect(result.value).to.be.equals(1); + }); +}); diff --git a/16 Immutablejs/src/actions/uiInputMember.ts b/16 Immutablejs/src/actions/uiInputMember.ts new file mode 100644 index 0000000..b1ea83e --- /dev/null +++ b/16 Immutablejs/src/actions/uiInputMember.ts @@ -0,0 +1,11 @@ +const uiInputMember = (fieldName : string, value: any) => { + return { + type: 'MEMBER_UI_INPUT' + ,fieldName : fieldName + ,value: value + } +} + +export { + uiInputMember +}; diff --git a/16 Immutablejs/src/api/memberAPI.ts b/16 Immutablejs/src/api/memberAPI.ts new file mode 100644 index 0000000..a8a3204 --- /dev/null +++ b/16 Immutablejs/src/api/memberAPI.ts @@ -0,0 +1,89 @@ +import MemberEntity from './memberEntity' +import MembersMockData from './memberMockData' +import * as _ from 'lodash' +import * as $ from 'jquery' +import * as Q from 'q' +import http from '../http/http'; + +// Sync mock data API, inspired from: +// https://gist.github.com/coryhouse/fd6232f95f9d601158e4 +class MemberAPI { + private _idSeed : number; + + public constructor() { + this._idSeed = 20; + } + + //This would be performed on the server in a real app. Just stubbing in. + private _clone (item) { + return JSON.parse(JSON.stringify(item)); //return cloned copy so that the item is passed by value instead of by reference + }; + + //This would be performed on the server in a real app. Just stubbing in. + _generateId() : number { + return this._idSeed++; + }; + + + // Just return a copy of the mock data + getAllMembers() : Array { + return this._clone(MembersMockData); + } + + // Not 100% clean we have to pass the dispatcher here + // TODO: Enhance proposal, if this is a singleton we can + // just initialize http with the dispatcher in a entry + // point (app init or something like that) + getAllMembersAsync() : Q.Promise { + // Going more modern: check 'fetch' and ES6 Promise + var deferred = Q.defer>(); + + // TODO: Only handling success, pending handling error + http.Get('https://api.github.com/orgs/lemoncode/members').then( + function(data) { + var members : Array; + + members = data.map((gitHubMember) => { + var member : MemberEntity = new MemberEntity(); + + member.id = gitHubMember.id; + member.login = gitHubMember.login; + member.avatar_url = gitHubMember.avatar_url; + + return member; + }); + + deferred.resolve(members); + } + ); + + return deferred.promise; + } + + + getMemberById(id : number) : MemberEntity { + var member = _.find(MembersMockData, {id: id}); + return this._clone(member); + } + + saveAuthor(member: MemberEntity) { + //pretend an ajax call to web api is made here + console.log('Pretend this just saved the author to the DB via AJAX call...'); + + if (member.id != -1) { + var existingAuthorIndex = _.indexOf(MembersMockData, _.find(MembersMockData, {id: member.id})); + MembersMockData.splice(existingAuthorIndex, 1, member); + } else { + //Just simulating creation here. + //The server would generate ids for new authors in a real app. + //Cloning so copy returned is passed by value rather than by reference. + member.id = this._generateId(); + MembersMockData.push(this._clone(member)); + } + + return member; + } + +} + +export default new MemberAPI(); diff --git a/16 Immutablejs/src/api/memberEntity.ts b/16 Immutablejs/src/api/memberEntity.ts new file mode 100644 index 0000000..844c376 --- /dev/null +++ b/16 Immutablejs/src/api/memberEntity.ts @@ -0,0 +1,12 @@ + +export default class MemberEntity { + id: number; + login: string; + avatar_url: string; + + constructor() { + this.id = -1; + this.login = ""; + this.avatar_url = ""; + } +} diff --git a/16 Immutablejs/src/api/memberMockData.ts b/16 Immutablejs/src/api/memberMockData.ts new file mode 100644 index 0000000..5c3e2a5 --- /dev/null +++ b/16 Immutablejs/src/api/memberMockData.ts @@ -0,0 +1,17 @@ +import MemberEntity from './memberEntity' + +var MembersMockData : Array = + [ + { + id: 1457912, + login: "brauliodiez", + avatar_url: "https://avatars.githubusercontent.com/u/1457912?v=3" + }, + { + id: 4374977, + login: "Nasdan", + avatar_url: "https://avatars.githubusercontent.com/u/4374977?v=3" + } + ]; + + export default MembersMockData; diff --git a/16 Immutablejs/src/api/repoAPI.ts b/16 Immutablejs/src/api/repoAPI.ts new file mode 100644 index 0000000..1a04b22 --- /dev/null +++ b/16 Immutablejs/src/api/repoAPI.ts @@ -0,0 +1,36 @@ +import RepoEntity from './repoEntity'; +import * as Q from 'q'; +import http from '../http/http'; + +class RepoAPI { + + //This would be performed on the server in a real app. Just stubbing in. + private _clone (item) { + return JSON.parse(JSON.stringify(item)); //return cloned copy so that the item is passed by value instead of by reference + }; + + getAllReposAsync(): Q.Promise { + var deferred = Q.defer>(); + + http.Get('https://api.github.com/orgs/lemoncode/repos').then( + function (data) { + var repos: Array; + + repos = data.map(gitHubRepo => { + var repo: RepoEntity = new RepoEntity(); + + repo.id = gitHubRepo.id; + repo.name = gitHubRepo.name + + return repo; + }); + + deferred.resolve(repos); + } + ); + + return deferred.promise; + } +} + +export default new RepoAPI(); diff --git a/16 Immutablejs/src/api/repoEntity.ts b/16 Immutablejs/src/api/repoEntity.ts new file mode 100644 index 0000000..9403067 --- /dev/null +++ b/16 Immutablejs/src/api/repoEntity.ts @@ -0,0 +1,9 @@ +export default class RepoEntity { + id: number; + name: string; + + constructor(){ + this.id = -1; + this.name = ""; + } +}; diff --git a/16 Immutablejs/src/components/about/aboutPage.tsx b/16 Immutablejs/src/components/about/aboutPage.tsx new file mode 100644 index 0000000..85e96d9 --- /dev/null +++ b/16 Immutablejs/src/components/about/aboutPage.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import header from '../common/header'; +import {Link} from 'react-router'; + +interface Props { +} + +export default class About extends React.Component { + public render() { + return ( +
+

About Page

+ Members +
+ ); + } +} diff --git a/16 Immutablejs/src/components/about/spec/aboutPage.spec.tsx b/16 Immutablejs/src/components/about/spec/aboutPage.spec.tsx new file mode 100644 index 0000000..c9c0186 --- /dev/null +++ b/16 Immutablejs/src/components/about/spec/aboutPage.spec.tsx @@ -0,0 +1,14 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { Link } from 'react-router'; +import AboutPage from '../aboutPage'; + +describe('AboutPage presentational component', () =>{ + it('should renders an h2 element with text "About Page"', () => { + let aboutPageWrapper = shallow(); + + expect(aboutPageWrapper.find('h2')).to.be.exist; + expect(aboutPageWrapper.find('h2').text()).to.be.equals('About Page'); + }); +}) diff --git a/16 Immutablejs/src/components/app.tsx b/16 Immutablejs/src/components/app.tsx new file mode 100644 index 0000000..85acadc --- /dev/null +++ b/16 Immutablejs/src/components/app.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { createStore, applyMiddleware } from 'redux'; +import { Provider } from 'react-redux'; +import Header from './common/header'; +import reducers from '../reducers'; +import ReduxThunk from 'redux-thunk'; +import SpinnerContainer from './common/spinner.container'; + +interface Props extends React.Props { +} + +let store = createStore( + reducers + ,applyMiddleware(ReduxThunk) +); + +export default class App extends React.Component { + public render() { + return ( + +
+ +
+ {this.props.children} +
+
+ ); + } +} diff --git a/16 Immutablejs/src/components/common/header.tsx b/16 Immutablejs/src/components/common/header.tsx new file mode 100644 index 0000000..4b42362 --- /dev/null +++ b/16 Immutablejs/src/components/common/header.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import {Link} from 'react-router'; + +interface Props { +} + +export default class Header extends React.Component { + public render() { + return ( +
+ +
+ ); + } +} diff --git a/16 Immutablejs/src/components/common/spec/header.spec.tsx b/16 Immutablejs/src/components/common/spec/header.spec.tsx new file mode 100644 index 0000000..8c70bf1 --- /dev/null +++ b/16 Immutablejs/src/components/common/spec/header.spec.tsx @@ -0,0 +1,20 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Header from '../header'; +import { Link } from 'react-router'; + +describe('Header presentational component', () =>{ + it('should renders two Link elements, first one with propert to equals "/about" and ' + + 'text equals "About" and second one with property to equals "/members" and text' + + 'equals "Members"', () => { + let headerWrapper = shallow(
); + + expect(headerWrapper.find('Link').at(0)).to.be.exist; + expect(headerWrapper.find('Link').at(0).prop('to')).to.be.equals("/about"); + expect(headerWrapper.find('Link').at(0).children().text()).to.be.equals("About"); + expect(headerWrapper.find('Link').at(1)).to.be.exist; + expect(headerWrapper.find('Link').at(1).prop('to')).to.be.equals("/members"); + expect(headerWrapper.find('Link').at(1).children().text()).to.be.equals("Members"); + }); +}) diff --git a/16 Immutablejs/src/components/common/spec/spinner.container.spec.tsx b/16 Immutablejs/src/components/common/spec/spinner.container.spec.tsx new file mode 100644 index 0000000..ce3f6be --- /dev/null +++ b/16 Immutablejs/src/components/common/spec/spinner.container.spec.tsx @@ -0,0 +1,76 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import SpinnerContainer from '../spinner.container'; +import { Provider } from 'react-redux'; +import configureStore = require('redux-mock-store'); +import * as httpActions from '../../../actions/httpInitializeDispatcher'; + +const createStore = configureStore(); +describe('Spinner container component', () =>{ + it('should renders Spinner presentational component and this has property showSpinner equals undefined and ' + + 'it calls to httpInitializeDispatcher when passing to store state equals ' + + '{ http: { httpCallsInProgress: undefined } }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + http: { + httpCallsInProgress: undefined + } + }); + + let httpInitializeDispatcherActionStub = sinon.stub(httpActions, 'httpInitializeDispatcher'); + + let spinnerWrapper = mount( + + + + ); + + expect(spinnerWrapper.find('Spinner').prop('showSpinner')).to.be.undefined; + expect(httpInitializeDispatcherActionStub.called).to.be.true; + }).bind(this)); + + it('should renders Spinner presentational component and this has property showSpinner equals false and ' + + 'it calls to httpInitializeDispatcher when passing to store state equals ' + + '{ http: { httpCallsInProgress: false } }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + http: { + httpCallsInProgress: false + } + }); + + let httpInitializeDispatcherActionStub = sinon.stub(httpActions, 'httpInitializeDispatcher'); + + let spinnerWrapper = mount( + + + + ); + + expect(spinnerWrapper.find('Spinner').prop('showSpinner')).to.be.false; + expect(httpInitializeDispatcherActionStub.called).to.be.true; + }).bind(this)); + + it('should renders Spinner presentational component and this has property showSpinner equals true and ' + + 'it calls to httpInitializeDispatcher when passing to store state equals ' + + '{ http: { httpCallsInProgress: true } }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + http: { + httpCallsInProgress: true + } + }); + + let httpInitializeDispatcherActionStub = sinon.stub(httpActions, 'httpInitializeDispatcher'); + + let spinnerWrapper = mount( + + + + ); + + expect(spinnerWrapper.find('Spinner').prop('showSpinner')).to.be.true; + expect(httpInitializeDispatcherActionStub.called).to.be.true; + }).bind(this)); +}); diff --git a/16 Immutablejs/src/components/common/spec/spinner.spec.tsx b/16 Immutablejs/src/components/common/spec/spinner.spec.tsx new file mode 100644 index 0000000..66ef422 --- /dev/null +++ b/16 Immutablejs/src/components/common/spec/spinner.spec.tsx @@ -0,0 +1,55 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Spinner from '../spinner'; + +describe('Spinner presentational component', () =>{ + it('should renders null and calls to initializeHttp method when passing showSpinner ' + + 'equals undefined and initializeHttp method like props', () => { + let showSpinner = undefined; + let initializeHttp = sinon.spy(); + let spinnerWrapper = shallow( + + ); + + expect(spinnerWrapper.type()).to.be.null; + expect(initializeHttp.calledOnce).to.be.true; + }); + + it('should renders null and calls to initializeHttp method when passing showSpinner ' + + 'equals null and initializeHttp method like props', () => { + let showSpinner = null; + let initializeHttp = sinon.spy(); + let spinnerWrapper = shallow( + + ); + + expect(spinnerWrapper.type()).to.be.null; + expect(initializeHttp.calledOnce).to.be.true; + }); + + it('should renders null and calls to initializeHttp method when passing showSpinner ' + + 'equals false and initializeHttp method like props', () => { + let showSpinner = false; + let initializeHttp = sinon.spy(); + let spinnerWrapper = shallow( + + ); + + expect(spinnerWrapper.type()).to.be.null; + expect(initializeHttp.calledOnce).to.be.true; + }); + + it('should renders div element with class equals "spinnerWrap" and calls to initializeHttp method ' + + 'when passing showSpinner equals true and initializeHttp method like props', () => { + let showSpinner = true; + let initializeHttp = sinon.spy(); + let spinnerWrapper = shallow( + + ); + + expect(spinnerWrapper.type()).to.be.equals('div'); + expect(spinnerWrapper.hasClass('spinnerWrap')).to.be.true; + expect(initializeHttp.calledOnce).to.be.true; + }); +}) diff --git a/16 Immutablejs/src/components/common/spec/textInput.spec.tsx b/16 Immutablejs/src/components/common/spec/textInput.spec.tsx new file mode 100644 index 0000000..68096ad --- /dev/null +++ b/16 Immutablejs/src/components/common/spec/textInput.spec.tsx @@ -0,0 +1,289 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Input from '../textInput'; + +describe('Input presentational component', () =>{ + it('should renders a div element with class equals "form-group" and this div has 2 childrens ' + + 'passing required properties with undefined value', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + expect(textInputWrapper.type()).to.be.equals('div'); + expect(textInputWrapper.hasClass('form-group')).to.be.true; + expect(textInputWrapper.hasClass('has-error')).to.be.false; + expect(textInputWrapper.children()).to.have.length(2); + }); + + it('should renders a div element with class equals "form-group has-error" and this div has 2 childrens ' + + 'passing error equals "test"', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: 'test' + }; + + let textInputWrapper = shallow( + + ); + + expect(textInputWrapper.type()).to.be.equals('div'); + expect(textInputWrapper.hasClass('form-group')).to.be.true; + expect(textInputWrapper.hasClass('has-error')).to.be.true; + expect(textInputWrapper.children()).to.have.length(2); + }); + + it('should renders a label element like first child with htmlFor property equals undefined ' + + 'and text equals empty passing name and label equals undefined', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + expect(textInputWrapper.children().at(0).type()).to.be.equals('label'); + expect(textInputWrapper.children().at(0).prop('htmlFor')).to.be.undefined; + expect(textInputWrapper.children().at(0).text()).to.be.equals(''); + }); + + it('should renders a label element like first child with htmlFor property equals "test name" ' + + 'and text equals "test label" passing name equals "test name" and label equals "test label"', () => { + let props = { + name: 'test name', + label: 'test label', + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + expect(textInputWrapper.children().at(0).type()).to.be.equals('label'); + expect(textInputWrapper.children().at(0).prop('htmlFor')).to.be.equals('test name'); + expect(textInputWrapper.children().at(0).text()).to.be.equals('test label'); + }); + + it('should renders a div element like second child class equals "field" ' + + 'passing required properties with undefined value', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + expect(textInputWrapper.children().at(1).type()).to.be.equals('div'); + expect(textInputWrapper.children().at(1).hasClass('field')).to.be.true; + }); + + it('should renders a div element like second child with 2 childrens inside input and div' + + 'passing required properties with undefined value', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + expect(textInputWrapper.children().at(1).type()).to.be.equals('div'); + expect(textInputWrapper.children().at(1).children()).to.have.length(2); + expect(textInputWrapper.children().at(1).children().at(0).type()).to.be.equals('input'); + expect(textInputWrapper.children().at(1).children().at(1).type()).to.be.equals('div'); + }); + + it('should renders an input with type property equals "text" class equals "form-control" ' + + 'and rest of the properties equals undefined passing required properties equals undefined', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + let inputElementWrapper = textInputWrapper.children().at(1).children().at(0); + expect(inputElementWrapper.type()).to.be.equals('input'); + expect(inputElementWrapper.prop('type')).to.be.equals('text'); + expect(inputElementWrapper.hasClass('form-control')).to.be.true; + expect(inputElementWrapper.prop('name')).to.be.undefined; + expect(inputElementWrapper.prop('placeholder')).to.be.undefined; + expect(inputElementWrapper.prop('ref')).to.be.undefined; + expect(inputElementWrapper.prop('value')).to.be.undefined; + expect(inputElementWrapper.prop('onChange')).to.be.undefined; + }); + + it('should renders an input with name and ref properties equals "test"' + + 'passing name equals "test"', () => { + let props = { + name: 'test', + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + let inputElementWrapper = textInputWrapper.children().at(1).children().at(0); + expect(inputElementWrapper.type()).to.be.equals('input'); + expect(inputElementWrapper.prop('name')).to.be.equals('test'); + }); + + it('should renders an input with placeholder property equals "test"' + + 'passing placeholder equals "test"', () => { + let props = { + name: undefined, + placeholder: 'test', + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + let inputElementWrapper = textInputWrapper.children().at(1).children().at(0); + expect(inputElementWrapper.type()).to.be.equals('input'); + expect(inputElementWrapper.prop('placeholder')).to.be.equals('test'); + }); + + it('should renders an input with value property equals "test"' + + 'passing value equals "test"', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: 'test', + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + let inputElementWrapper = textInputWrapper.children().at(1).children().at(0); + expect(inputElementWrapper.type()).to.be.equals('input'); + expect(inputElementWrapper.prop('value')).to.be.equals('test'); + }); + + it('should renders an input with onChange property equals function ' + + 'passing mockedOnChange method', () => { + let props = { + name: undefined, + label: undefined, + onChange: sinon.spy(), + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + let inputElementWrapper = textInputWrapper.children().at(1).children().at(0); + expect(inputElementWrapper.type()).to.be.equals('input'); + expect(inputElementWrapper.prop('onChange')).to.be.a('function'); + expect(props.onChange.called).to.be.false; + }); + + it('should renders an input with onChange property equals function and calls to onChange' + + 'passing mockedOnChange method when user write on input', () => { + let props = { + name: undefined, + label: undefined, + onChange: sinon.spy(), + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + let inputElementWrapper = textInputWrapper.children().at(1).children().at(0); + inputElementWrapper.simulate('change'); + + expect(inputElementWrapper.type()).to.be.equals('input'); + expect(inputElementWrapper.prop('onChange')).to.be.a('function'); + expect(props.onChange.calledOnce).to.be.true; + }); + + it('should renders an div with class equals "input" and text equals empty' + + 'passing error property equals undefined', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: undefined + }; + + let textInputWrapper = shallow( + + ); + + let divElementWrapper = textInputWrapper.children().at(1).children().at(1); + + expect(divElementWrapper.type()).to.be.equals('div'); + expect(divElementWrapper.hasClass('input')).to.be.true; + expect(divElementWrapper.text()).to.be.equals(''); + }); + + it('should renders an div with class equals "input" and text equals "test"' + + 'passing error property equals "test"', () => { + let props = { + name: undefined, + label: undefined, + onChange: undefined, + value: undefined, + error: 'test' + }; + + let textInputWrapper = shallow( + + ); + + let divElementWrapper = textInputWrapper.children().at(1).children().at(1); + + expect(divElementWrapper.type()).to.be.equals('div'); + expect(divElementWrapper.hasClass('input')).to.be.true; + expect(divElementWrapper.text()).to.be.equals('test'); + }); +}) diff --git a/16 Immutablejs/src/components/common/spinner.container.tsx b/16 Immutablejs/src/components/common/spinner.container.tsx new file mode 100644 index 0000000..eed6651 --- /dev/null +++ b/16 Immutablejs/src/components/common/spinner.container.tsx @@ -0,0 +1,22 @@ +import { connect } from 'react-redux'; +import { httpInitializeDispatcher } from '../../actions/httpInitializeDispatcher'; +import Spinner from './spinner'; + +let mapStateToProps = (state) => { + return { + showSpinner: state.http.httpCallsInProgress + } +} + +let mapDispatchToProps = (dispatch) => { + return { + initializeHttp: () => {return dispatch(httpInitializeDispatcher(dispatch))} + } +} + +let SpinnerContainer = connect( + mapStateToProps, + mapDispatchToProps +)(Spinner) + +export default SpinnerContainer; diff --git a/16 Immutablejs/src/components/common/spinner.tsx b/16 Immutablejs/src/components/common/spinner.tsx new file mode 100644 index 0000000..cc44723 --- /dev/null +++ b/16 Immutablejs/src/components/common/spinner.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +interface Props { + showSpinner? : boolean; + initializeHttp? : () => void; +} + +export default class Spinner extends React.Component { + constructor(props : Props){ + super(props); + this.props.initializeHttp(); + } + + public render() { + if (!this.props.showSpinner) { + return null; + } + + return ( +
+
+
+
+ Loading... +
+
+
+ ); + } +} diff --git a/16 Immutablejs/src/components/common/textInput.tsx b/16 Immutablejs/src/components/common/textInput.tsx new file mode 100644 index 0000000..a2b2d58 --- /dev/null +++ b/16 Immutablejs/src/components/common/textInput.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; + +interface Props { + name : string; + label : string; + onChange : any; + placeholder? : string; + value: string; + error : string; +} + +// This components just contains a wrapper to avoid adding repetitive +// code to each input (label indicating error, onChange callback, ...) +// this is a port to typescript from Cory House sample +// for more advanced scenario, rather check +// https://github.com/christianalfoni/formsy-react +export default class Input extends React.Component { + constructor(props : Props){ + super(props); + } + + public render() { + var wrapperClass : string = 'form-group'; + if (this.props.error && this.props.error.length > 0) { + wrapperClass += " " + 'has-error'; + } + return ( +
+ +
+ +
{this.props.error}
+
+
+ ); + } +} diff --git a/16 Immutablejs/src/components/member/memberForm.tsx b/16 Immutablejs/src/components/member/memberForm.tsx new file mode 100644 index 0000000..5f4351b --- /dev/null +++ b/16 Immutablejs/src/components/member/memberForm.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import MemberEntity from '../../api/memberEntity'; +import MemberErrors from '../../validations/memberFormErrors'; +import Input from '../common/textInput'; + +interface Props extends React.Props { + member : MemberEntity + onChange : (event) => void; + onSave : (event) => void; + errors: MemberErrors; +} + +export default class MemberForm extends React.Component { + constructor(props : Props){ + super(props); + } + + public render() { + return ( +
+

Manage member

+ + + + + + +
+ ); + } +} diff --git a/16 Immutablejs/src/components/member/memberPage.container.tsx b/16 Immutablejs/src/components/member/memberPage.container.tsx new file mode 100644 index 0000000..4daae32 --- /dev/null +++ b/16 Immutablejs/src/components/member/memberPage.container.tsx @@ -0,0 +1,35 @@ +import { connect } from 'react-redux'; +import { loadMember } from '../../actions/loadMember'; +import { uiInputMember } from '../../actions/uiInputMember'; +import MemberEntity from '../../api/memberEntity'; +import { saveMember } from '../../actions/saveMember'; +import { resetSaveCompleted } from '../../actions/resetSaveCompleted'; +import { initializeNewMember } from '../../actions/initializeNewMember'; +import MemberPage from './memberPage'; + +let mapStateToProps = (state) => { + return { + member: state.member.member + ,errors : state.member.errors + ,saveCompleted : state.member.saveCompleted + } +} + +let mapDispatchToProps = (dispatch) => { + return { + loadMember: (id : number) => {return dispatch(loadMember(id))} + ,fireValidationFieldValueChanged: (fieldName : string, value : any) => {return dispatch(uiInputMember(fieldName, value))} + ,saveMember: (member: MemberEntity) => {return dispatch(saveMember(member))} + ,resetSaveCompletedFlag: () => {return dispatch(resetSaveCompleted())} + ,initializeNewMember: () => {return dispatch(initializeNewMember()) + } + } +} + +let ContainerMemberPage = connect( + mapStateToProps, + mapDispatchToProps +)(MemberPage) + + +export default ContainerMemberPage; diff --git a/16 Immutablejs/src/components/member/memberPage.tsx b/16 Immutablejs/src/components/member/memberPage.tsx new file mode 100644 index 0000000..df17159 --- /dev/null +++ b/16 Immutablejs/src/components/member/memberPage.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { hashHistory } from 'react-router'; +import * as toastr from 'toastr'; +import MemberEntity from '../../api/memberEntity'; +import MemberErrors from '../../validations/memberFormErrors'; +import MemberForm from './memberForm'; + +interface Props extends React.Props { + params? : any + member? : MemberEntity + ,errors?: MemberErrors + ,saveCompleted? : boolean + ,loadMember? : (id : number) => void + ,fireValidationFieldValueChanged? : (fieldName : string, value : any) => void + ,saveMember?: (member: MemberEntity) => void + ,initializeNewMember?: () => void + ,resetSaveCompletedFlag?: () => void +} + +export default class MemberPage extends React.Component { + constructor(props : Props){ + super(props); + } + + componentWillMount() { + let memberId = this.getMemberId(); + + if(memberId) { + this.props.loadMember(memberId); + } else { + this.props.initializeNewMember(); + } + } + + // https://github.com/reactjs/redux/issues/580 + componentWillReceiveProps(nextProps) { + if(this.props.saveCompleted != nextProps.saveCompleted + && nextProps.saveCompleted) { + + // Show toast + toastr.success('Author saved.'); + + // using hashHistory, TODO: proper configure browserHistory on app and here + hashHistory.push('/members') + + // Reset saveCompleted flag + this.props.resetSaveCompletedFlag(); + } + } + + private getMemberId() : number { + // Coming from navigation + return this.props.params && this.props.params.id ? + parseInt(this.props.params.id) : + null; + } + + // on any update on the form this function will be called + private updateMemberFromUI(event) { + var field = event.target.name; + var value = event.target.value; + + this.props.fireValidationFieldValueChanged(field, value); + } + + private saveMember(event) { + event.preventDefault(); + + this.props.saveMember(this.props.member); + } + + public render() { + if(!this.props.member) + return (
No data
) + + return ( + + ); + } +} diff --git a/16 Immutablejs/src/components/member/spec/memberForm.spec.tsx b/16 Immutablejs/src/components/member/spec/memberForm.spec.tsx new file mode 100644 index 0000000..d54338c --- /dev/null +++ b/16 Immutablejs/src/components/member/spec/memberForm.spec.tsx @@ -0,0 +1,427 @@ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import * as React from 'react'; +import MemberForm from '../memberForm'; +import MemberEntity from '../../../api/memberEntity'; +import MemberErrors from '../../../validations/memberFormErrors'; +import Input from '../../common/textInput'; + +describe('MemberForm presentational component', () => { + it('should renders a form with 4 children h1, Input, Input and input' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + expect(memberFormWrapper.type()).to.be.equals('form'); + expect(memberFormWrapper.children().at(0).type()).to.be.equals('h1'); + expect(memberFormWrapper.children().at(1).type()).to.be.equals(Input); + expect(memberFormWrapper.children().at(2).type()).to.be.equals(Input); + expect(memberFormWrapper.children().at(3).type()).to.be.equals('input'); + }); + + it('should renders a h1 with text equals "Manage member"' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let h1Wrapper = memberFormWrapper.children().at(0); + expect(h1Wrapper.type()).to.be.equals('h1'); + expect(h1Wrapper.text()).to.be.equals('Manage member'); + }); + + it('should renders first Input with property name equals "login"' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(1); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('name')).to.be.equals('login'); + }); + + it('should renders first Input with property label equals "Login"' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(1); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('label')).to.be.equals('Login'); + }); + + it('should renders first Input with property value equals empty' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(1); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('value')).to.be.equals(''); + }); + + it('should renders first Input with property value equals "test"' + + 'passing member equals { login: "test" }', () => { + let member = new MemberEntity(); + member.login = "test"; + + let properties = { + member: member, + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(1); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('value')).to.be.equals('test'); + }); + + it('should renders first Input with onChange property equals function and calls to onChangeMock ' + + 'passing mockedOnChange method when user write on input', () => { + let onChangeMock = sinon.spy(); + + let properties = { + member: new MemberEntity(), + onChange: onChangeMock, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(1); + inputWrapper.simulate('change'); + + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('onChange')).to.be.a('function'); + expect(onChangeMock.calledOnce).to.be.true; + }); + + it('should renders first Input with property error equals empty' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(1); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('error')).to.be.equals(''); + }); + + it('should renders first Input with property error equals "test"' + + 'passing errors equals { login: "test" }', () => { + let errors = new MemberErrors(); + errors.login = "test"; + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: errors + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(1); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('error')).to.be.equals('test'); + }); + + it('should renders second Input with property name equals "avatar_url"' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(2); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('name')).to.be.equals('avatar_url'); + }); + + it('should renders second Input with property label equals "Avatar Url"' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(2); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('label')).to.be.equals('Avatar Url'); + }); + + it('should renders second Input with property value equals empty' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(2); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('value')).to.be.equals(''); + }); + + it('should renders second Input with property value equals "test"' + + 'passing member equals { avatar_url: "test" }', () => { + let member = new MemberEntity(); + member.avatar_url = "test"; + + let properties = { + member: member, + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(2); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('value')).to.be.equals('test'); + }); + + it('should renders second Input with onChange property equals function and calls to onChangeMock ' + + 'passing mockedOnChange method when user write on input', () => { + let onChangeMock = sinon.spy(); + + let properties = { + member: new MemberEntity(), + onChange: onChangeMock, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(2); + inputWrapper.simulate('change'); + + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('onChange')).to.be.a('function'); + expect(onChangeMock.calledOnce).to.be.true; + }); + + it('should renders second Input with property error equals empty' + + 'passing required properties with default values', () => { + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(2); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('error')).to.be.equals(''); + }); + + it('should renders second Input with property error equals "test"' + + 'passing errors equals { avatar_url: "test" }', () => { + let errors = new MemberErrors(); + errors.avatar_url = "test"; + + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: errors + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(2); + expect(inputWrapper.type()).to.be.equals(Input); + expect(inputWrapper.prop('error')).to.be.equals('test'); + }); + + it('should renders a input with property type equals "submit"' + + 'passing required properties with default values', () => { + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(3); + expect(inputWrapper.type()).to.be.equals('input'); + expect(inputWrapper.prop('type')).to.be.equals('submit'); + }); + + it('should renders a input with property value equals "Save"' + + 'passing required properties with default values', () => { + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(3); + expect(inputWrapper.type()).to.be.equals('input'); + expect(inputWrapper.prop('value')).to.be.equals('Save'); + }); + + it('should renders a input with property class equals "btn btn-default"' + + 'passing required properties with default values', () => { + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(3); + expect(inputWrapper.type()).to.be.equals('input'); + expect(inputWrapper.hasClass('btn')).to.be.true; + expect(inputWrapper.hasClass('btn-default')).to.be.true; + }); + + it('should renders a input with property onClick equals undefined' + + 'passing required properties with default values', () => { + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: undefined, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(3); + expect(inputWrapper.type()).to.be.equals('input'); + expect(inputWrapper.prop('onClick')).to.be.equals(undefined); + }); + + it('should renders a input with property onClick equals function and calls to onSaveMock' + + 'passing onSave equals onSaveMock and user click on input', () => { + let onSaveMock = sinon.spy(); + let properties = { + member: new MemberEntity(), + onChange: undefined, + onSave: onSaveMock, + errors: new MemberErrors() + }; + + let memberFormWrapper = shallow( + + ); + + let inputWrapper = memberFormWrapper.children().at(3); + inputWrapper.simulate('click'); + + + expect(inputWrapper.type()).to.be.equals('input'); + expect(inputWrapper.prop('onClick')).to.be.a('function'); + expect(onSaveMock.calledOnce).to.be.true; + }); +}); diff --git a/16 Immutablejs/src/components/member/spec/memberPage.container.spec.tsx b/16 Immutablejs/src/components/member/spec/memberPage.container.spec.tsx new file mode 100644 index 0000000..5926a2f --- /dev/null +++ b/16 Immutablejs/src/components/member/spec/memberPage.container.spec.tsx @@ -0,0 +1,302 @@ +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import * as React from 'react'; +import configureStore = require('redux-mock-store'); +import MemberPageContainer from '../memberPage.container'; +import MemberEntity from '../../../api/memberEntity'; +import MemberErrors from '../../../validations/memberFormErrors'; +import * as initializeMemberActions from '../../../actions/initializeNewMember'; +import * as loadMemberActions from '../../../actions/loadMember'; +import * as uiInputMemberActions from '../../../actions/uiInputMember'; +import * as saveMemberActions from '../../../actions/saveMember'; +import * as resetSaveCompletedActions from '../../../actions/resetSaveCompleted'; + +const createStore = configureStore(); + +describe('MemberPage container component', () => { + it('should renders MemberPage presentational component with member property equals undefined' + + 'passing state equals { member: { member: undefined } }', () => { + + let mockStore = createStore({ + member: { + member: undefined + } + }); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('member')).to.be.undefined; + expect(memberPagePresentationalWrapper.prop('errors')).to.be.undefined; + expect(memberPagePresentationalWrapper.prop('saveCompleted')).to.be.undefined; + }); + + it('should renders MemberPage presentational component with member property equals { id: 1 } ' + + 'passing state equals { member: { member: { id: 1 } } }', () => { + let member = new MemberEntity(); + member.id = 1; + + let mockStore = createStore({ + member: { + member: member, + errors: new MemberErrors() //We have to initialize due to deep childrens + } + }); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('member')).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('member').id).to.be.equals(member.id); + }); + + it('should renders MemberPage presentational component with errors property equals undefined' + + 'passing state equals { member: { errors: undefined } }', () => { + + let mockStore = createStore({ + member: { + errors: undefined + } + }); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('errors')).to.be.undefined; + }); + + it('should renders MemberPage presentational component with errors property equals { login: "test" }' + + 'passing state equals { member: { errors: { login: "test" } } }', () => { + let errors = new MemberErrors(); + errors.login = "test"; + + let mockStore = createStore({ + member: { + errors: errors + } + }); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('errors')).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('errors').login).to.be.equals(errors.login); + }); + + it('should renders MemberPage presentational component with saveCompleted property equals undefined' + + 'passing state equals { member: { saveCompleted: undefined} }', () => { + + let mockStore = createStore({ + member: { + saveCompleted: undefined + } + }); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('saveCompleted')).to.be.undefined; + }); + + it('should renders MemberPage presentational component with saveCompleted property equals false' + + 'passing state equals { member: { saveCompleted: false} }', () => { + + let mockStore = createStore({ + member: { + saveCompleted: false + } + }); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('saveCompleted')).to.be.false; + }); + + it('should renders MemberPage presentational component and calls to initializeNewMember' + + 'passing state equals { member: { } }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + member: { + + } + }); + + let initializeNewMemberMock = sinon.stub(initializeMemberActions, 'initializeNewMember'); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(initializeNewMemberMock.calledOnce).to.be.true; + }).bind(this)); + + it('should renders MemberPage presentational component and does not call to loadMember' + + 'passing state equals { member: { } }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + member: { + + } + }); + + let loadMemberActionsMock = sinon.stub(loadMemberActions, 'loadMember'); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(loadMemberActionsMock.calledOnce).to.be.false; + }).bind(this)); + + it('should renders MemberPage presentational component and calls to loadMember(1)' + + 'passing state equals { member: { } } and property params equals { id: 1 }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + member: { + + } + }); + + let params = { + id: 1 + }; + + let loadMemberActionsMock = sinon.stub(loadMemberActions, 'loadMember'); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(loadMemberActionsMock.calledOnce).to.be.true; + expect(loadMemberActionsMock.calledWith(params.id)).to.be.true; + }).bind(this)); + + it('should renders MemberPage presentational component and calls to uiInputMember("testField", "testValue")' + + 'passing state equals { member: { } } and calling to fireValidationFieldValueChanged property with ' + + 'parameters "testField" and "testValue"', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + member: { + + } + }); + + let uiInputMemberActionsMock = sinon.stub(uiInputMemberActions, 'uiInputMember'); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + let fireValidationFieldValueChanged = memberPagePresentationalWrapper.prop('fireValidationFieldValueChanged'); + fireValidationFieldValueChanged("testField", "testValue"); + + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('fireValidationFieldValueChanged')).not.to.be.undefined; + expect(uiInputMemberActionsMock.called).to.be.true; + expect(uiInputMemberActionsMock.calledWith("testField", "testValue")).to.be.true; + }).bind(this)); + + it('should renders MemberPage presentational component and calls to saveMember(member)' + + 'passing state equals { member: { } } and calling to saveMember property with parameter member', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + member: { + + } + }); + + let saveMemberActionsMock = sinon.stub(saveMemberActions, 'saveMember'); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + let saveMember = memberPagePresentationalWrapper.prop('saveMember'); + let member = new MemberEntity(); + member.id = 1; + + saveMember(member); + + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('saveMember')).not.to.be.undefined; + expect(saveMemberActionsMock.called).to.be.true; + expect(saveMemberActionsMock.calledWith(member)).to.be.true; + }).bind(this)); + + it('should renders MemberPage presentational component and calls to resetSaveCompleted(member)' + + 'passing state equals { member: { } } and calling to resetSaveCompletedFlag', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + member: { + + } + }); + + let resetSaveCompletedActionsMock = sinon.stub(resetSaveCompletedActions, 'resetSaveCompleted'); + + let memberPageContainerWrapper = mount( + + + + ); + + let memberPagePresentationalWrapper = memberPageContainerWrapper.find('MemberPage'); + let resetSaveCompletedFlag = memberPagePresentationalWrapper.prop('resetSaveCompletedFlag'); + resetSaveCompletedFlag(); + + expect(memberPagePresentationalWrapper).not.to.be.undefined; + expect(memberPagePresentationalWrapper.prop('resetSaveCompletedFlag')).not.to.be.undefined; + expect(resetSaveCompletedActionsMock.called).to.be.true; + expect(resetSaveCompletedActionsMock.calledWith()).to.be.true; + }).bind(this)); +}); diff --git a/16 Immutablejs/src/components/member/spec/memberPage.spec.tsx b/16 Immutablejs/src/components/member/spec/memberPage.spec.tsx new file mode 100644 index 0000000..4969575 --- /dev/null +++ b/16 Immutablejs/src/components/member/spec/memberPage.spec.tsx @@ -0,0 +1,447 @@ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import * as React from 'react'; +import MemberPage from '../memberPage'; +import MemberEntity from '../../../api/memberEntity'; +import MemberForm from '../memberForm'; +import MemberErrors from '../../../validations/memberFormErrors'; +import * as toastr from 'toastr'; +import { hashHistory } from 'react-router'; + +describe('MemberPage presentational component', () => { + it('should renders a div with text equals "No data" and calls to initializeNewMember' + + 'passing required properties with default values', () => { + let initializeNewMemberMock = sinon.spy(); + + let properties = { + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals('div'); + expect(memberPageWrapper.text()).to.be.equals('No data'); + expect(initializeNewMemberMock.called).to.be.true; + }); + + //"sinson.test" make automatic cleanup instead of use sinon.restore + //https://semaphoreci.com/community/tutorials/best-practices-for-spies-stubs-and-mocks-in-sinon-js + it('should calls to componentWillMount' + + 'passing required properties with default values', sinon.test(() => { + //Just to get tsd instellisense + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let componentWillMountMock = sinon.stub(MemberPage.prototype, 'componentWillMount'); + + let properties = { + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(componentWillMountMock.calledOnce).to.be.true; + }).bind(this)); + + it('should renders a div with text equals "No data" and calls to initializeNewMember' + + 'passing params property equals 1', () => { + let initializeNewMemberMock = sinon.spy(); + let loadMemberMock = sinon.spy(); + + let properties = { + params: 1, + initializeNewMember: initializeNewMemberMock, + loadMember: loadMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals('div'); + expect(memberPageWrapper.text()).to.be.equals('No data'); + expect(initializeNewMemberMock.called).to.be.true; + expect(loadMemberMock.called).to.be.false; + expect(loadMemberMock.calledWith(1)).to.be.false; + }); + + it('should renders a div with text equals "No data" and calls to initializeNewMember' + + 'passing params property equals "test"', () => { + let initializeNewMemberMock = sinon.spy(); + let loadMemberMock = sinon.spy(); + + let properties = { + params: "test", + initializeNewMember: initializeNewMemberMock, + loadMember: loadMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals('div'); + expect(memberPageWrapper.text()).to.be.equals('No data'); + expect(initializeNewMemberMock.called).to.be.true; + expect(loadMemberMock.called).to.be.false; + expect(loadMemberMock.calledWith(1)).to.be.false; + }); + + it('should renders a div with text equals "No data" and calls to initializeNewMember' + + 'passing params property equals { id: "test" }', () => { + let initializeNewMemberMock = sinon.spy(); + let loadMemberMock = sinon.spy(); + + let properties = { + params: { + id: "test" + }, + initializeNewMember: initializeNewMemberMock, + loadMember: loadMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals('div'); + expect(memberPageWrapper.text()).to.be.equals('No data'); + expect(initializeNewMemberMock.called).to.be.true; + expect(loadMemberMock.called).to.be.false; + expect(loadMemberMock.calledWith(1)).to.be.false; + }); + + it('should renders a div with text equals "No data" and calls to loadMember' + + 'passing params property equals { id: 1 }', () => { + let initializeNewMemberMock = sinon.spy(); + let loadMemberMock = sinon.spy(); + + let properties = { + params: { + id: 1 + }, + initializeNewMember: initializeNewMemberMock, + loadMember: loadMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals('div'); + expect(memberPageWrapper.text()).to.be.equals('No data'); + expect(initializeNewMemberMock.called).to.be.false; + expect(loadMemberMock.called).to.be.true; + expect(loadMemberMock.calledWith(1)).to.be.true; + }); + + it('should renders a MemberForm with member equals { id: 1 } and calls to initializeNewMember' + + 'passing member equals { id: 1 } and params equals undefined', () => { + let initializeNewMemberMock = sinon.spy(); + let member = new MemberEntity(); + member.id = 1; + + let properties = { + initializeNewMember: initializeNewMemberMock, + member: member, + params: undefined + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals(MemberForm); + expect(memberPageWrapper.prop('member')).not.to.be.undefined; + expect(memberPageWrapper.prop('member').id).to.be.equals(member.id); + expect(initializeNewMemberMock.called).to.be.true; + }); + + it('should renders a MemberForm with errors property equals undefined' + + 'passing errors equals undefined', () => { + let initializeNewMemberMock = sinon.spy(); + let member = new MemberEntity(); + member.id = 1; + + let properties = { + initializeNewMember: initializeNewMemberMock, + member: member, + errors: undefined + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals(MemberForm); + expect(memberPageWrapper.prop('errors')).to.be.undefined; + }); + + it('should renders a MemberForm with errors property equals { login: "test" }' + + 'passing errors equals { login: "test" }', () => { + let initializeNewMemberMock = sinon.spy(); + let member = new MemberEntity(); + member.id = 1; + + let errors = new MemberErrors(); + errors.login = "test"; + + let properties = { + initializeNewMember: initializeNewMemberMock, + member: member, + errors: errors + }; + + let memberPageWrapper = shallow( + + ); + + expect(memberPageWrapper.type()).to.be.equals(MemberForm); + expect(memberPageWrapper.prop('errors')).not.to.be.undefined; + expect(memberPageWrapper.prop('errors').login).to.be.equals('test'); + }); + + it('should renders a MemberForm and calls to fireValidationFieldValueChanged with ' + + 'arguments field equals "testField" and value equals "testValue" ' + + 'when user write in element with name property equals "testField" and ' + + 'value property equals "testValue"', () => { + let initializeNewMemberMock = sinon.spy(); + let member = new MemberEntity(); + member.id = 1; + + let fireValidationMock = sinon.spy(); + + let properties = { + initializeNewMember: initializeNewMemberMock, + member: member, + fireValidationFieldValueChanged: fireValidationMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.simulate('change', { + target: { + name: "testField", + value: "testValue" + } + }) + + expect(fireValidationMock.called).to.be.true; + expect(fireValidationMock.calledWith("testField", "testValue")).to.be.true; + }); + + it('should renders a MemberForm and calls to saveMember with arguments ' + + 'member equals member property and calls to preventDefault ' + + 'when user click on save button', () => { + let initializeNewMemberMock = sinon.spy(); + let member = new MemberEntity(); + member.id = 1; + + let saveMemberMock = sinon.spy(); + let eventPreventDefaultMock = sinon.spy(); + + let properties = { + initializeNewMember: initializeNewMemberMock, + member: member, + saveMember: saveMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.simulate('save', { + preventDefault: eventPreventDefaultMock + }) + + expect(saveMemberMock.called).to.be.true; + expect(saveMemberMock.calledWith(member)).to.be.true; + expect(eventPreventDefaultMock.called).to.be.true; + }); + + it('should does not call to componentWillReceiveProps' + + 'passing required properties with default values', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let componentWillReceivePropsMock = sinon.stub(MemberPage.prototype, 'componentWillReceiveProps'); + + let properties = { + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(componentWillReceivePropsMock.calledOnce).to.be.false; + }).bind(this)); + + it('should does not call to componentWillReceiveProps' + + 'passing saveCompleted equals false', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let componentWillReceivePropsMock = sinon.stub(MemberPage.prototype, 'componentWillReceiveProps'); + + let properties = { + saveCompleted: false, + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + expect(componentWillReceivePropsMock.calledOnce).to.be.false; + }).bind(this)); + + it('should calls to componentWillReceiveProps but does not calls to toastr.success()' + + 'passing saveCompleted equals false and setting root component props with ' + + 'saveCompleted property equals false', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let componentWillReceivePropsMock = sinon.stub(MemberPage.prototype, 'componentWillReceiveProps'); + let toastrMock = sinon.stub(toastr, 'success'); + + let properties = { + saveCompleted: false, + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.setProps({ + saveCompleted: false + }); + + expect(componentWillReceivePropsMock.calledOnce).to.be.true; + expect(toastrMock.calledOnce).to.be.false; + }).bind(this)); + + it('should calls to componentWillReceiveProps but does not calls to toastr.success()' + + 'passing saveCompleted equals true and setting root component props with ' + + 'saveCompleted property equals true', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let toastrMock = sinon.stub(toastr, 'success'); + + let properties = { + saveCompleted: true, + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.setProps({ + saveCompleted: true + }); + + expect(toastrMock.calledOnce).to.be.false; + }).bind(this)); + + it('should calls to componentWillReceiveProps but does not calls to toastr.success()' + + 'passing saveCompleted equals true and setting root component props with ' + + 'saveCompleted property equals false', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let toastrMock = sinon.stub(toastr, 'success'); + + let properties = { + saveCompleted: true, + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.setProps({ + saveCompleted: false + }); + + expect(toastrMock.calledOnce).to.be.false; + }).bind(this)); + + it('should calls to componentWillReceiveProps and toastr.success("Author saved.")' + + 'passing saveCompleted equals false and setting root component props with ' + + 'saveCompleted property equals true', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let resetSaveCompletedFlagMock = sinon.spy(); + let toastrMock = sinon.stub(toastr, 'success'); + + let properties = { + saveCompleted: false, + resetSaveCompletedFlag: resetSaveCompletedFlagMock, + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.setProps({ + saveCompleted: true + }); + + expect(toastrMock.calledOnce).to.be.true; + expect(toastrMock.calledWith('Author saved.')).to.be.true; + }).bind(this)); + + it('should calls to hashHistory.push("/members")' + + 'passing saveCompleted equals false and setting root component props with ' + + 'saveCompleted property equals true', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let initializeNewMemberMock = sinon.spy(); + let resetSaveCompletedFlagMock = sinon.spy(); + let hashHistoryMock = sinon.stub(hashHistory, 'push'); + + let properties = { + saveCompleted: false, + resetSaveCompletedFlag: resetSaveCompletedFlagMock, + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.setProps({ + saveCompleted: true + }); + + expect(hashHistoryMock.calledOnce).to.be.true; + expect(hashHistoryMock.calledWith('/members')).to.be.true; + }).bind(this)); + + it('should calls to resetSaveCompletedFlag' + + 'passing saveCompleted equals false and setting root component props with ' + + 'saveCompleted property equals true', () => { + let initializeNewMemberMock = sinon.spy(); + let resetSaveCompletedFlagMock = sinon.spy(); + + let properties = { + saveCompleted: false, + resetSaveCompletedFlag: resetSaveCompletedFlagMock, + initializeNewMember: initializeNewMemberMock + }; + + let memberPageWrapper = shallow( + + ); + + memberPageWrapper.setProps({ + saveCompleted: true + }); + + expect(resetSaveCompletedFlagMock.calledOnce).to.be.true; + }); +}); diff --git a/16 Immutablejs/src/components/members/memberList.tsx b/16 Immutablejs/src/components/members/memberList.tsx new file mode 100644 index 0000000..61eb9bc --- /dev/null +++ b/16 Immutablejs/src/components/members/memberList.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import MemberEntity from '../../api/memberEntity'; +import MemberRow from './memberRow'; +import {Link} from 'react-router'; + +interface Props extends React.Props{ + members: Array; +} + +export default class MemberList extends React.Component{ + constructor(props: Props){ + super(props); + } + + render() { + return ( +
+

Members Page

+ New Member + + + + + + + + + + { + this.props.members.map((member : MemberEntity) => + + ) + } + +
+ Avatar + + Id + + Name +
+
+ ); + } +} diff --git a/16 Immutablejs/src/components/members/memberRow.tsx b/16 Immutablejs/src/components/members/memberRow.tsx new file mode 100644 index 0000000..6786458 --- /dev/null +++ b/16 Immutablejs/src/components/members/memberRow.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import {Link} from 'react-router'; +import MemberEntity from '../../api/memberEntity'; + +interface Props extends React.Props { + member : MemberEntity; +} + +export default class MemberRow extends React.Component { + + constructor(props : Props){ + super(props); + } + + public render() { + return ( + + + + + + {this.props.member.id} + + + {this.props.member.login} + + + ); + } +} diff --git a/16 Immutablejs/src/components/members/membersPage.container.tsx b/16 Immutablejs/src/components/members/membersPage.container.tsx new file mode 100644 index 0000000..41140de --- /dev/null +++ b/16 Immutablejs/src/components/members/membersPage.container.tsx @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { loadMembers } from '../../actions/loadMembers'; +import { loadRepos } from '../../actions/loadRepos'; +import MembersPage from './membersPage'; + +const mapStateToProps = (state) => { + return { + members: state.members, + repos: state.repos + } +} + +const mapDispatchToProps = (dispatch) => { + return { + loadMembers: () => { return dispatch(loadMembers()) }, + loadRepos: () => { return dispatch(loadRepos()) } + } +} + +const ContainerMembersPage = connect( + mapStateToProps, + mapDispatchToProps +)(MembersPage) + +export default ContainerMembersPage; diff --git a/16 Immutablejs/src/components/members/membersPage.tsx b/16 Immutablejs/src/components/members/membersPage.tsx new file mode 100644 index 0000000..0247da7 --- /dev/null +++ b/16 Immutablejs/src/components/members/membersPage.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import MemberEntity from '../../api/memberEntity'; +import MemberList from './memberList'; +import RepoEntity from '../../api/repoEntity'; +import RepoList from '../repos/repoList'; + +interface Props extends React.Props{ + members? : Array; + loadMembers? : () => void; + repos? : Array; + loadRepos? : () => void; +} + +export default class MembersPage extends React.Component { + + // Standard react lifecycle function: + // https://facebook.github.io/react/docs/component-specs.html + public componentDidMount() { + this.props.loadMembers(); + this.props.loadRepos(); + } + + public render() { + if(!this.props.members || !this.props.repos) + return (
No data
) + + return ( +
+ + +
+ ); + } +} diff --git a/16 Immutablejs/src/components/members/spec/memberList.spec.tsx b/16 Immutablejs/src/components/members/spec/memberList.spec.tsx new file mode 100644 index 0000000..e9c4a66 --- /dev/null +++ b/16 Immutablejs/src/components/members/spec/memberList.spec.tsx @@ -0,0 +1,129 @@ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { Link } from 'react-router'; +import MemberList from '../memberList'; +import MemberEntity from '../../../api/memberEntity'; +import MemberRow from '../memberRow'; + +describe('MemberList presentational component', () => { + it('should renders a div, h2 element like child with text equals "Members Page" ' + + 'passing members equals empty', () => { + let members = []; + + let memberListWrapper = shallow( + + ); + + expect(memberListWrapper.type()).to.be.equals('div'); + expect(memberListWrapper.children().at(0).type()).to.be.equals('h2'); + expect(memberListWrapper.children().at(0).text()).to.be.equals('Members Page'); + }); + + it('should renders a div, Link element like child with to property equals "/member" and ' + + 'text equals "Members Page" passing members equals empty', () => { + let members = []; + + let memberListWrapper = shallow( + + ); + + expect(memberListWrapper.type()).to.be.equals('div'); + expect(memberListWrapper.children().at(1).type()).to.be.equals(Link); + expect(memberListWrapper.children().at(1).prop('to')).to.be.equals('/member'); + expect(memberListWrapper.children().at(1).children().text()).to.be.equals('New Member'); + }); + + it('should renders a div, table element like child with class equals "table" ' + + 'passing members equals empty', () => { + let members = []; + + let memberListWrapper = shallow( + + ); + + expect(memberListWrapper.type()).to.be.equals('div'); + expect(memberListWrapper.children().at(2).type()).to.be.equals('table'); + expect(memberListWrapper.children().at(2).hasClass('table')).to.be.true; + }); + + it('should renders a div, table element like child with 3 head columns "Avatar" "Id" and "Name"' + + 'passing members equals empty', () => { + let members = []; + + let memberListWrapper = shallow( + + ); + + expect(memberListWrapper.type()).to.be.equals('div'); + expect(memberListWrapper.children().at(2).type()).to.be.equals('table'); + expect(memberListWrapper.children().at(2).find('thead').contains( + + + + Avatar + + + Id + + + Name + + + + )).to.be.true; + }); + + it('should renders a div, table element like child with empty tbody element' + + 'passing members equals empty', () => { + let members = []; + + let memberListWrapper = shallow( + + ); + + expect(memberListWrapper.type()).to.be.equals('div'); + expect(memberListWrapper.children().at(2).type()).to.be.equals('table'); + expect(memberListWrapper.children().at(2).find('tbody').html()).to.be.equals(''); + }); + + it('should renders a div, table element like child with one MemberRow element inside tbody element' + + 'with key property equals 1 and member property equals member' + + 'passing members equals [member]', () => { + let member = new MemberEntity(); + member.id = 1; + + let members = [member]; + + let memberListWrapper = shallow( + + ); + + expect(memberListWrapper.find('tbody').children().at(0).type()).to.be.equals(MemberRow); + expect(memberListWrapper.find('tbody').children().get(0).key).to.be.equals(member.id.toString()); + expect(memberListWrapper.find('tbody').children().at(0).prop('member')).to.be.equals(member); + expect(memberListWrapper.find('tbody').children().at(1).type()).to.be.null; + }); + + it('should renders a div, table element like child with two MemberRow elements inside tbody element' + + 'with key property equals 1 and 2 and member property equals member1 and member2' + + 'passing members equals [member1, member2]', () => { + let member1 = new MemberEntity(); + member1.id = 1; + let member2 = new MemberEntity(); + member2.id = 2; + + let members = [member1, member2]; + + let memberListWrapper = shallow( + + ); + + expect(memberListWrapper.find('tbody').children().at(0).type()).to.be.equals(MemberRow); + expect(memberListWrapper.find('tbody').children().get(0).key).to.be.equals(member1.id.toString()); + expect(memberListWrapper.find('tbody').children().at(0).prop('member')).to.be.equals(member1); + expect(memberListWrapper.find('tbody').children().get(1).key).to.be.equals(member2.id.toString()); + expect(memberListWrapper.find('tbody').children().at(1).prop('member')).to.be.equals(member2); + expect(memberListWrapper.find('tbody').children().at(2).type()).to.be.null; + }); +}); diff --git a/16 Immutablejs/src/components/members/spec/memberRow.spec.tsx b/16 Immutablejs/src/components/members/spec/memberRow.spec.tsx new file mode 100644 index 0000000..89a5402 --- /dev/null +++ b/16 Immutablejs/src/components/members/spec/memberRow.spec.tsx @@ -0,0 +1,112 @@ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { Link } from 'react-router'; +import MemberRow from '../memberRow'; +import MemberEntity from '../../../api/memberEntity'; + +describe('MemberRow presentational component', () => { + it('should renders a tr element with three td like children' + + 'passing member property with default values', () => { + let member = new MemberEntity(); + + let memberRowWrapper = shallow( + + ); + + expect(memberRowWrapper.type()).to.be.equals('tr'); + expect(memberRowWrapper.children().at(0).type()).to.be.equals('td'); + expect(memberRowWrapper.children().at(1).type()).to.be.equals('td'); + expect(memberRowWrapper.children().at(2).type()).to.be.equals('td'); + expect(memberRowWrapper.children().at(3).type()).to.be.null; + }); + + it('should renders a img element in first column with class equals "avatar" ' + + 'and src property equals empty' + + 'passing member property with default values', () => { + let member = new MemberEntity(); + + let memberRowWrapper = shallow( + + ); + + expect(memberRowWrapper.type()).to.be.equals('tr'); + expect(memberRowWrapper.children().at(0).children().type()).to.be.equals('img'); + expect(memberRowWrapper.children().at(0).children().hasClass('avatar')).to.be.true; + expect(memberRowWrapper.children().at(0).children().prop('src')).to.be.empty; + }); + + it('should renders a img element in first column with class equals "avatar" ' + + 'and src property equals "test"' + + 'passing member property equals { avatar_url: "test"}', () => { + let member = new MemberEntity(); + member.avatar_url = "test"; + + let memberRowWrapper = shallow( + + ); + + expect(memberRowWrapper.type()).to.be.equals('tr'); + expect(memberRowWrapper.children().at(0).children().type()).to.be.equals('img'); + expect(memberRowWrapper.children().at(0).children().hasClass('avatar')).to.be.true; + expect(memberRowWrapper.children().at(0).children().prop('src')).to.be.equals('test'); + }); + + it('should renders a Link element in second column with to property equals "/memberEdit/-1" ' + + 'and text equals -1' + + 'passing member property with default values', () => { + let member = new MemberEntity(); + + let memberRowWrapper = shallow( + + ); + + expect(memberRowWrapper.type()).to.be.equals('tr'); + expect(memberRowWrapper.children().at(1).children().type()).to.be.equals(Link); + expect(memberRowWrapper.children().at(1).children().prop('to')).to.be.equals('/memberEdit/-1'); + expect(memberRowWrapper.children().at(1).children().children().text()).to.be.equals('-1'); + }); + + it('should renders a Link element in second column with to property equals "/memberEdit/2" ' + + 'and text equals 2' + + 'passing member property equals { id: 2 }', () => { + let member = new MemberEntity(); + member.id = 2 + + let memberRowWrapper = shallow( + + ); + + expect(memberRowWrapper.type()).to.be.equals('tr'); + expect(memberRowWrapper.children().at(1).children().type()).to.be.equals(Link); + expect(memberRowWrapper.children().at(1).children().prop('to')).to.be.equals('/memberEdit/2'); + expect(memberRowWrapper.children().at(1).children().children().text()).to.be.equals('2'); + }); + + it('should renders a span element in third column with text equals empty ' + + 'passing member property with default values', () => { + let member = new MemberEntity(); + + let memberRowWrapper = shallow( + + ); + + expect(memberRowWrapper.type()).to.be.equals('tr'); + expect(memberRowWrapper.children().at(2).children().type()).to.be.equals('span'); + expect(memberRowWrapper.children().at(2).children().text()).to.be.empty; + }); + + it('should renders a span element in third column with text equals "test" ' + + 'passing member property equals { login: "test" }', () => { + let member = new MemberEntity(); + member.login = "test"; + + let memberRowWrapper = shallow( + + ); + + expect(memberRowWrapper.type()).to.be.equals('tr'); + expect(memberRowWrapper.children().at(2).children().type()).to.be.equals('span'); + expect(memberRowWrapper.children().at(2).children().text()).to.be.equals('test'); + }); +}); diff --git a/16 Immutablejs/src/components/members/spec/membersPage.container.spec.tsx b/16 Immutablejs/src/components/members/spec/membersPage.container.spec.tsx new file mode 100644 index 0000000..b0fde53 --- /dev/null +++ b/16 Immutablejs/src/components/members/spec/membersPage.container.spec.tsx @@ -0,0 +1,156 @@ +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import * as React from 'react'; +import configureStore = require('redux-mock-store'); +import MembersPageContainer from "../membersPage.container"; +import MemberEntity from '../../../api/memberEntity'; +import RepoEntity from '../../../api/repoEntity'; +import * as loadMemberActions from '../../../actions/loadMembers'; +import * as loadRepoActions from '../../../actions/loadRepos'; + +const createStore = configureStore(); + +describe('MembersPage container component', () => { + it('should renders MembersPage presentational component with members property equals undefined' + + 'passing state equals { members: undefined }', () => { + let mockStore = createStore({ + members: undefined + }); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(membersPagePresentationalWrapper).not.to.be.undefined; + expect(membersPagePresentationalWrapper.prop('members')).to.be.undefined; + }); + + it('should renders MembersPage presentational component with members property equals empty' + + 'passing state equals { members: new Array() }', () => { + let mockStore = createStore({ + members: new Array() + }); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(membersPagePresentationalWrapper).not.to.be.undefined; + expect(membersPagePresentationalWrapper.prop('members')).to.be.empty; + }); + + it('should renders MembersPage presentational component with members property equals array with one member' + + 'passing state equals { members: [member] }', () => { + let member = new MemberEntity(); + + let mockStore = createStore({ + members: [member] + }); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(membersPagePresentationalWrapper).not.to.be.undefined; + expect(membersPagePresentationalWrapper.prop('members')).to.have.length(1); + }); + + it('should renders MembersPage presentational component with repos property equals undefined' + + 'passing state equals { repos: undefined }', () => { + let mockStore = createStore({ + repos: undefined + }); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(membersPagePresentationalWrapper).not.to.be.undefined; + expect(membersPagePresentationalWrapper.prop('repos')).to.be.undefined; + }); + + it('should renders MembersPage presentational component with repos property equals empty' + + 'passing state equals { repos: new Array() }', () => { + let mockStore = createStore({ + repos: new Array() + }); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(membersPagePresentationalWrapper).not.to.be.undefined; + expect(membersPagePresentationalWrapper.prop('repos')).to.be.empty; + }); + + it('should renders MembersPage presentational component with repos property equals array with one repo' + + 'passing state equals { repos: [repo] }', () => { + let repo = new RepoEntity(); + + let mockStore = createStore({ + repos: [repo] + }); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(membersPagePresentationalWrapper).not.to.be.undefined; + expect(membersPagePresentationalWrapper.prop('repos')).to.have.length(1); + }); + + it('should renders MembersPage presentational component and calls to loadMembers' + + 'passing state equals { }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + }); + + let loadMemberActionsMock = sinon.stub(loadMemberActions, 'loadMembers'); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(loadMemberActionsMock.calledOnce).to.be.true; + }).bind(this)); + + it('should renders MembersPage presentational component and calls to loadRepos' + + 'passing state equals { }', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let mockStore = createStore({ + }); + + let loadRepoActionsMock = sinon.stub(loadRepoActions, 'loadRepos'); + + let membersPageContainerWrapper = mount( + + + + ); + + var membersPagePresentationalWrapper = membersPageContainerWrapper.find('MembersPage'); + expect(loadRepoActionsMock.calledOnce).to.be.true; + }).bind(this)); +}); diff --git a/16 Immutablejs/src/components/members/spec/membersPage.spec.tsx b/16 Immutablejs/src/components/members/spec/membersPage.spec.tsx new file mode 100644 index 0000000..3c18489 --- /dev/null +++ b/16 Immutablejs/src/components/members/spec/membersPage.spec.tsx @@ -0,0 +1,170 @@ +import { expect } from 'chai'; +import { shallow, mount } from "enzyme"; +import * as React from "react"; +import MembersPage from '../membersPage'; +import MemberEntity from '../../../api/memberEntity'; +import RepoEntity from '../../../api/repoEntity'; +import MemberList from '../memberList'; +import RepoList from "../../repos/repoList"; + +describe('MembersPage presentational component', () => { + it('should renders a div with text equals "No data" and does not calls to loadMembers and loadRepos' + + 'passing members and repos properties equals undefined and using shallow enzyme method', () => { + let loadMembersMock = sinon.spy(); + let loadReposMock = sinon.spy(); + + let properties = { + members: undefined, + repos: undefined, + loadMembers: loadMembersMock, + loadRepos: loadReposMock + }; + + var membersPageWrapper = shallow( + + ); + + expect(membersPageWrapper.type()).to.be.equals('div'); + expect(membersPageWrapper.text()).to.be.equals('No data'); + expect(loadMembersMock.calledOnce).to.be.false; + expect(loadReposMock.calledOnce).to.be.false; + }); + + it('should renders a div with text equals "No data" and does not calls to loadMembers and loadRepos' + + 'passing members equals empty array and repos equals undefined and using shallow enzyme method', () => { + let loadMembersMock = sinon.spy(); + let loadReposMock = sinon.spy(); + + let properties = { + members: new Array(), + repos: undefined, + loadMembers: loadMembersMock, + loadRepos: loadReposMock + }; + + var membersPageWrapper = shallow( + + ); + + expect(membersPageWrapper.type()).to.be.equals('div'); + expect(membersPageWrapper.text()).to.be.equals('No data'); + expect(loadMembersMock.calledOnce).to.be.false; + expect(loadReposMock.calledOnce).to.be.false; + }); + + it('should renders a div with text equals "No data" and does not calls to loadMembers and loadRepos' + + 'passing members equals undefined and repos equals empty array and using shallow enzyme method', () => { + let loadMembersMock = sinon.spy(); + let loadReposMock = sinon.spy(); + + let properties = { + members: undefined, + repos: new Array(), + loadMembers: loadMembersMock, + loadRepos: loadReposMock + }; + + var membersPageWrapper = shallow( + + ); + + expect(membersPageWrapper.type()).to.be.equals('div'); + expect(membersPageWrapper.text()).to.be.equals('No data'); + expect(loadMembersMock.calledOnce).to.be.false; + expect(loadReposMock.calledOnce).to.be.false; + }); + + it('should renders a div with class equals "row" and two children, MemberList with property members equals empty ' + + 'and RepoList with property repos equals empty, and does not calls to loadMembers and loadRepos' + + 'passing members equals empty array and repos equals empty array and using shallow enzyme method', () => { + let loadMembersMock = sinon.spy(); + let loadReposMock = sinon.spy(); + + let properties = { + members: new Array(), + repos: new Array(), + loadMembers: loadMembersMock, + loadRepos: loadReposMock + }; + + var membersPageWrapper = shallow( + + ); + + expect(membersPageWrapper.type()).to.be.equals('div'); + expect(membersPageWrapper.hasClass('row')).to.be.true; + expect(membersPageWrapper.children().at(0).type()).to.be.equals(MemberList); + expect(membersPageWrapper.children().at(0).prop('members')).to.be.empty; + expect(membersPageWrapper.children().at(1).type()).to.be.equals(RepoList); + expect(membersPageWrapper.children().at(1).prop('repos')).to.be.empty; + expect(loadMembersMock.calledOnce).to.be.false; + expect(loadReposMock.calledOnce).to.be.false; + }); + + it('should renders a div with class equals "row" and two children, MemberList with property members equals array with one member ' + + 'and RepoList with property repos equals array with one repo, and does not calls to loadMembers and loadRepos' + + 'passing members equals [member] and repos equals [repo] array and using shallow enzyme method', () => { + let loadMembersMock = sinon.spy(); + let loadReposMock = sinon.spy(); + + let member = new MemberEntity(); + let repo = new RepoEntity(); + + let properties = { + members: [member], + repos: [repo], + loadMembers: loadMembersMock, + loadRepos: loadReposMock + }; + + var membersPageWrapper = shallow( + + ); + + expect(membersPageWrapper.type()).to.be.equals('div'); + expect(membersPageWrapper.hasClass('row')).to.be.true; + expect(membersPageWrapper.children().at(0).type()).to.be.equals(MemberList); + expect(membersPageWrapper.children().at(0).prop('members')).to.have.length(1); + expect(membersPageWrapper.children().at(1).type()).to.be.equals(RepoList); + expect(membersPageWrapper.children().at(1).prop('repos')).to.have.length(1); + expect(loadMembersMock.calledOnce).to.be.false; + expect(loadReposMock.calledOnce).to.be.false; + }); + + it('should calls to componentDidMount' + + 'passing members and repos properties equals undefined and using mount enzyme method', sinon.test(() => { + let sinon: Sinon.SinonStatic = this; + let componentDidMountMock = sinon.stub(MembersPage.prototype, 'componentDidMount'); + + let properties = { + members: undefined, + repos: undefined + }; + + var membersPageWrapper = mount( + + ); + + expect(componentDidMountMock.calledOnce).to.be.true; + }).bind(this)); + + it('should calls toloadMembers and loadRepos' + + 'passing members and repos properties equals undefined and using mount enzyme method', () => { + let loadMembersMock = sinon.spy(); + let loadReposMock = sinon.spy(); + + let properties = { + members: undefined, + repos: undefined, + loadMembers: loadMembersMock, + loadRepos: loadReposMock + }; + + var membersPageWrapper = mount( + + ); + + expect(loadMembersMock.calledOnce).to.be.true; + expect(loadReposMock.calledOnce).to.be.true; + }); +}); diff --git a/16 Immutablejs/src/components/repos/repoList.tsx b/16 Immutablejs/src/components/repos/repoList.tsx new file mode 100644 index 0000000..1724688 --- /dev/null +++ b/16 Immutablejs/src/components/repos/repoList.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import RepoEntity from '../../api/repoEntity'; +import RepoRow from './repoRow'; + +interface Props extends React.Props{ + repos: Array; +} + +export default class RepoList extends React.Component{ + constructor(props: Props) { + super(props); + } + + render(){ + return ( +
+

Repos

+ + + + + + + + + { + this.props.repos.map((repo: RepoEntity) => + + ) + } + +
+ Id + + Name +
+
+ ); + } +} diff --git a/16 Immutablejs/src/components/repos/repoRow.tsx b/16 Immutablejs/src/components/repos/repoRow.tsx new file mode 100644 index 0000000..3d466b7 --- /dev/null +++ b/16 Immutablejs/src/components/repos/repoRow.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import RepoEntity from '../../api/repoEntity'; + +interface Props extends React.Props{ + repo : RepoEntity; +} + +export default class RepoRow extends React.Component { + constructor(props: Props){ + super(props); + } + + public render() { + return ( + + + {this.props.repo.id} + + + {this.props.repo.name} + + + ); + } +} diff --git a/16 Immutablejs/src/components/repos/spec/repoList.spec.tsx b/16 Immutablejs/src/components/repos/spec/repoList.spec.tsx new file mode 100644 index 0000000..54f41d2 --- /dev/null +++ b/16 Immutablejs/src/components/repos/spec/repoList.spec.tsx @@ -0,0 +1,112 @@ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { Link } from 'react-router'; +import RepoList from '../repoList'; +import RepoEntity from '../../../api/repoEntity'; +import RepoRow from '../repoRow'; + +describe('RepoList presentational component', () => { + it('should renders a div, h2 element like child with text equals "Repos" ' + + 'passing repos equals empty', () => { + let repos = []; + + let repoListWrapper = shallow( + + ); + + expect(repoListWrapper.type()).to.be.equals('div'); + expect(repoListWrapper.children().at(0).type()).to.be.equals('h2'); + expect(repoListWrapper.children().at(0).text()).to.be.equals('Repos'); + }); + + it('should renders a div, table element like child with class equals "table" ' + + 'passing repos equals empty', () => { + let repos = []; + + let repoListWrapper = shallow( + + ); + + expect(repoListWrapper.type()).to.be.equals('div'); + expect(repoListWrapper.children().at(1).type()).to.be.equals('table'); + expect(repoListWrapper.children().at(1).hasClass('table')).to.be.true; + }); + + it('should renders a div, table element like child with 2 head columns "Id" and "Name"' + + 'passing repos equals empty', () => { + let repos = []; + + let repoListWrapper = shallow( + + ); + + expect(repoListWrapper.type()).to.be.equals('div'); + expect(repoListWrapper.children().at(1).type()).to.be.equals('table'); + expect(repoListWrapper.children().at(1).find('thead').contains( + + + + Id + + + Name + + + + )).to.be.true; + }); + + it('should renders a div, table element like child with empty tbody element' + + 'passing repos equals empty', () => { + let repos = []; + + let repoListWrapper = shallow( + + ); + + expect(repoListWrapper.type()).to.be.equals('div'); + expect(repoListWrapper.children().at(1).type()).to.be.equals('table'); + expect(repoListWrapper.children().at(1).find('tbody').html()).to.be.equals(''); + }); + + it('should renders a div, table element like child with one RepoRow element inside tbody element' + + 'with key property equals 1 and repo property equals repo' + + 'passing repos equals [repo]', () => { + let repo = new RepoEntity(); + repo.id = 1; + + let repos = [repo]; + + let repoListWrapper = shallow( + + ); + + expect(repoListWrapper.find('tbody').children().at(0).type()).to.be.equals(RepoRow); + expect(repoListWrapper.find('tbody').children().get(0).key).to.be.equals(repo.id.toString()); + expect(repoListWrapper.find('tbody').children().at(0).prop('repo')).to.be.equals(repo); + expect(repoListWrapper.find('tbody').children().at(1).type()).to.be.null; + }); + + it('should renders a div, table element like child with two RepoRow elements inside tbody element' + + 'with key property equals 1 and 2 and repo property equals repo1 and repo2' + + 'passing repos equals [repo1, repo2]', () => { + let repo1 = new RepoEntity(); + repo1.id = 1; + let repo2 = new RepoEntity(); + repo2.id = 2; + + let repos = [repo1, repo2]; + + let repoListWrapper = shallow( + + ); + + expect(repoListWrapper.find('tbody').children().at(0).type()).to.be.equals(RepoRow); + expect(repoListWrapper.find('tbody').children().get(0).key).to.be.equals(repo1.id.toString()); + expect(repoListWrapper.find('tbody').children().at(0).prop('repo')).to.be.equals(repo1); + expect(repoListWrapper.find('tbody').children().get(1).key).to.be.equals(repo2.id.toString()); + expect(repoListWrapper.find('tbody').children().at(1).prop('repo')).to.be.equals(repo2); + expect(repoListWrapper.find('tbody').children().at(2).type()).to.be.null; + }); +}); diff --git a/16 Immutablejs/src/components/repos/spec/repoRow.spec.tsx b/16 Immutablejs/src/components/repos/spec/repoRow.spec.tsx new file mode 100644 index 0000000..64bd4a4 --- /dev/null +++ b/16 Immutablejs/src/components/repos/spec/repoRow.spec.tsx @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import * as React from 'react'; +import RepoRow from '../repoRow'; +import RepoEntity from '../../../api/repoEntity'; + +describe('RepoRow presentational component', () => { + it('should renders a tr element with two td like children' + + 'passing repo property with default values', () => { + let repo = new RepoEntity(); + + let repoRowWrapper = shallow( + + ); + + expect(repoRowWrapper.type()).to.be.equals('tr'); + expect(repoRowWrapper.children().at(0).type()).to.be.equals('td'); + expect(repoRowWrapper.children().at(1).type()).to.be.equals('td'); + expect(repoRowWrapper.children().at(2).type()).to.be.null; + }); + + it('should renders a span element in first column with text equals "-1" ' + + 'passing repo property with default values', () => { + let repo = new RepoEntity(); + + let repoRowWrapper = shallow( + + ); + + expect(repoRowWrapper.type()).to.be.equals('tr'); + expect(repoRowWrapper.children().at(0).children().type()).to.be.equals('span'); + expect(repoRowWrapper.children().at(0).children().text()).to.be.equals('-1'); + }); + + it('should renders a span element in first column with text equals "2" ' + + 'passing repo property equals { id: 2 }', () => { + let repo = new RepoEntity(); + repo.id = 2 + + let repoRowWrapper = shallow( + + ); + + expect(repoRowWrapper.type()).to.be.equals('tr'); + expect(repoRowWrapper.children().at(0).children().type()).to.be.equals('span'); + expect(repoRowWrapper.children().at(0).children().text()).to.be.equals('2'); + }); + + it('should renders a span element in second column with text equals empty ' + + 'passing repo property with default values', () => { + let repo = new RepoEntity(); + + let repoRowWrapper = shallow( + + ); + + expect(repoRowWrapper.type()).to.be.equals('tr'); + expect(repoRowWrapper.children().at(1).children().type()).to.be.equals('span'); + expect(repoRowWrapper.children().at(1).children().text()).to.be.empty; + }); + + it('should renders a span element in second column with text equals "test" ' + + 'passing repo property equals { name: "test" }', () => { + let repo = new RepoEntity(); + repo.name = "test"; + + let repoRowWrapper = shallow( + + ); + + expect(repoRowWrapper.type()).to.be.equals('tr'); + expect(repoRowWrapper.children().at(1).children().type()).to.be.equals('span'); + expect(repoRowWrapper.children().at(1).children().text()).to.be.equals('test'); + }); +}); diff --git a/16 Immutablejs/src/css/site.css b/16 Immutablejs/src/css/site.css new file mode 100644 index 0000000..d76da7a --- /dev/null +++ b/16 Immutablejs/src/css/site.css @@ -0,0 +1,48 @@ +/* entry point css */ +.avatar { + max-width: 80px +} + +/* <=Spinner AJAX request loading> */ +.spinnerWrap { + z-index: 40001; /* High z-index to ensure it appears above all content */ +} + +.vertical-offset { + /* Fixed position to provide the vertical offset */ + position: fixed; + top: 30%; + width: 100%; + z-index: 40002; /* ensures box appears above overlay */ +} + +.spinnerOverlay { + position: fixed; + left: 0; + width: 100%; + height: 100%; + background-color: black; + opacity: .5; /* Sets opacity so it's partly transparent */ + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; /* IE transparency */ + filter: alpha(opacity=50); /* More IE transparency */ + z-index: 40001; +} + + +div#spinner { + width: 100px; + height: 100px; + position: fixed; + top: 50%; + left: 50%; + background: url('../images/spinner.gif') no-repeat center #fff; + text-align: center; + padding: 10px; + font: normal 16px Tahoma, Geneva, sans-serif; + -moz-border-radius: 15px; + border-radius: 15px; + border: 1px solid #666; + margin-left: -50px; + margin-top: -50px; +} +/* <#Spinner AJAX request loading> */ diff --git a/16 Immutablejs/src/http/http.ts b/16 Immutablejs/src/http/http.ts new file mode 100644 index 0000000..b0ec012 --- /dev/null +++ b/16 Immutablejs/src/http/http.ts @@ -0,0 +1,43 @@ +import * as Q from 'q' +import * as $ from 'jquery' +import httpCallStarted from "../actions/httpCallStarted" +import httpCallCompleted from "../actions/httpCallCompleted" + + + +class Http { + _dispatcher : any; + + public Initialize(dispatcher) { + this._dispatcher = dispatcher; + } + + // ========================= + // Send dispatch command to increment in one the async calls counter + // Execute the async call action + // On Success, On Error --> Send dispatch comment to decrement + // Return promise + // ========================= + public Get(url : string) + { + var deferred = Q.defer(); + this._dispatcher(httpCallStarted()); + + // TODO: enhance this, better error handling + $.getJSON(url, function(data) { + this._dispatcher(httpCallCompleted()); + deferred.resolve(data); + }.bind(this) + ,function (err) { + this._dispatcher(httpCallCompleted()); + deferred.reject(err); + }.bind(this) + ); + + return deferred.promise; + + + } +} + +export default new Http(); diff --git a/16 Immutablejs/src/images/spinner.gif b/16 Immutablejs/src/images/spinner.gif new file mode 100644 index 0000000..cbe59fb Binary files /dev/null and b/16 Immutablejs/src/images/spinner.gif differ diff --git a/16 Immutablejs/src/index.html b/16 Immutablejs/src/index.html new file mode 100644 index 0000000..d1ef905 --- /dev/null +++ b/16 Immutablejs/src/index.html @@ -0,0 +1,11 @@ + + + + + + + +
+
+ + diff --git a/16 Immutablejs/src/index.tsx b/16 Immutablejs/src/index.tsx new file mode 100644 index 0000000..c22962b --- /dev/null +++ b/16 Immutablejs/src/index.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import App from './components/app.tsx' +import AboutPage from './components/about/aboutPage'; +import MembersPageContainer from './components/members/membersPage.container'; +import MemberPageContainer from './components/member/memberPage.container'; + +import { Router, Route, IndexRoute, Link, IndexLink, browserHistory, hashHistory } from 'react-router' + +ReactDOM.render( + + + + + + + + + + + , document.getElementById('root')); diff --git a/16 Immutablejs/src/reducers/httpReducer.ts b/16 Immutablejs/src/reducers/httpReducer.ts new file mode 100644 index 0000000..db2c4aa --- /dev/null +++ b/16 Immutablejs/src/reducers/httpReducer.ts @@ -0,0 +1,47 @@ +import objectAssign = require('object-assign'); +import http from '../http/http'; + +// Later on add more flags, like error or something like that? +class HttpState { + httpCallsInProgress : boolean; + numberOfCalls : number; + + constructor(){ + this.httpCallsInProgress = false; + this.numberOfCalls = 0; + } +} +// Just to show how combine reducers work, we have +// divided into two reducers member load + member load/update/delete +let httpReducer = (state = new HttpState(), action) => { + let newState : HttpState = null; + let numberOfCalls : number = null; + let callsInProgress : boolean = null; + + switch (action.type) { + case 'HTTP_GET_CALL_STARTED': + numberOfCalls = state.numberOfCalls + 1; + callsInProgress = true; + + newState = objectAssign({}, state, {httpCallsInProgress: callsInProgress, numberOfCalls: numberOfCalls}); + return newState; + + case 'HTTP_GET_CALL_COMPLETED': + numberOfCalls = state.numberOfCalls > 0 ? + state.numberOfCalls - 1 : + 0; + + callsInProgress = (numberOfCalls > 0); + + newState = objectAssign({}, state, {httpCallsInProgress: callsInProgress, numberOfCalls: numberOfCalls}); + return newState; + + default: + return state; + } +}; + +export { + HttpState, + httpReducer +} diff --git a/16 Immutablejs/src/reducers/index.ts b/16 Immutablejs/src/reducers/index.ts new file mode 100644 index 0000000..e88be31 --- /dev/null +++ b/16 Immutablejs/src/reducers/index.ts @@ -0,0 +1,30 @@ +/***** Important ******* +* You have to be careful with import Alias name if you want to use +* TypeScript/ES6 property shorthand notation eg. { contributors } +* with this notation means that you write the key like value name +* +* {contributorsReducer} =======> {contributorsReducer : contributorsReducer} +* +* import { combineReducers } from 'redux'; +* import contributorsReducer from './contributors'; +* +* export default combineReducers({ +* contributors: contributorsReducer +* }); +* +* contributors property is used in MapStateToProps method by state.contributors +* +* If you write {contributorsReducer} you have to use state.contributorsReducer +*/ +import { combineReducers } from 'redux'; +import { memberReducer as member } from './memberReducer'; +import members from './membersReducer'; +import { httpReducer as http } from './httpReducer'; +import repos from './reposReducer'; + +export default combineReducers({ + member + ,members + ,http + ,repos +}); diff --git a/16 Immutablejs/src/reducers/memberReducer.ts b/16 Immutablejs/src/reducers/memberReducer.ts new file mode 100644 index 0000000..6befc7d --- /dev/null +++ b/16 Immutablejs/src/reducers/memberReducer.ts @@ -0,0 +1,75 @@ +import { Map } from 'immutable'; +import MemberEntity from "../api/memberEntity"; +import MemberAPI from "../api/memberAPI"; +import objectAssign = require('object-assign'); +import MemberFormErrors from "../validations/memberFormErrors" +import MemberFormValidator from "../validations/memberFormValidator" + +class MemberState { + member : any; // MemberEntity + memberId : number; + errors : MemberFormErrors; + isValid : boolean; + saveCompleted : boolean; + + public constructor() + { + this.member = Map(new MemberEntity()); + this.memberId = -1; + this.errors = new MemberFormErrors(); + this.isValid = false; + this.saveCompleted = false; + } +} + +// Just to show how combine reducers work, we have +// divided into two reducers member load + member load/update/delete +let memberReducer = (state : MemberState = new MemberState(), action) => { + let newState : MemberState = null; + + switch (action.type) { + case 'MEMBER_INITIALIZE_NEW': + newState = objectAssign({}, state, {member: Map(new MemberEntity()), errors: new MemberFormErrors(), isValid: false}); + return newState; + + case 'MEMBER_LOAD': + newState = objectAssign({}, state, {dirty: false, member: Map(action.member), errors: new MemberFormErrors(), isValid: true}); + + return newState; + + case 'MEMBER_UI_INPUT': + const newMember = newState.member.set(action['fieldName'], action['value']); + newState = objectAssign({}, state, {member: newMember, dirty: true}); + + /* + let fieldName = action['fieldName']; + let value = action['value'] + + let newMember : MemberEntity = objectAssign({}, state.member, {}); + newMember[fieldName] = value; + + newState = objectAssign({}, state, {member: newMember, dirty: true}); + return newState; + */ + case 'MEMBER_SAVE': + if(action.errors.isEntityValid) { + newState = objectAssign({}, state, {saveCompleted: true}); + } else { + newState = objectAssign({}, state, {isValid: action.errors.isEntityValid, errors: action.errors}); + } + + return newState; + + case 'MEMBER_RESET_SAVE_COMPLETED': + newState = objectAssign({}, state, {saveCompleted: false}); + return newState; + + default: + return state; + } +}; + +export { + MemberState, + memberReducer +} diff --git a/16 Immutablejs/src/reducers/membersReducer.ts b/16 Immutablejs/src/reducers/membersReducer.ts new file mode 100644 index 0000000..34c7f87 --- /dev/null +++ b/16 Immutablejs/src/reducers/membersReducer.ts @@ -0,0 +1,14 @@ +import MemberEntity from "../api/memberEntity"; +import MemberAPI from "../api/memberAPI"; + +// Just to show how combine reducers work, we have +// divided into two reducers member load + member load/update/delete +export default (state : Array = [], action) => { + switch (action.type) { + case 'MEMBERS_ASSIGN': + return [...action.members]; + + default: + return state; + } +}; diff --git a/16 Immutablejs/src/reducers/reposReducer.ts b/16 Immutablejs/src/reducers/reposReducer.ts new file mode 100644 index 0000000..d0301cf --- /dev/null +++ b/16 Immutablejs/src/reducers/reposReducer.ts @@ -0,0 +1,11 @@ +import RepoEntity from '../api/repoEntity'; + +export default (state: Array = [], action) => { + switch (action.type){ + case 'REPOS_ASSIGN': + return [...action.repos]; + + default: + return state; + } +} diff --git a/16 Immutablejs/src/reducers/spec/httpReducer.spec.ts b/16 Immutablejs/src/reducers/spec/httpReducer.spec.ts new file mode 100644 index 0000000..d29d71e --- /dev/null +++ b/16 Immutablejs/src/reducers/spec/httpReducer.spec.ts @@ -0,0 +1,129 @@ +import { expect } from 'chai'; +import * as deepFreeze from 'deep-freeze'; +import { httpReducer, HttpState } from '../httpReducer'; + +describe('httpReducer', () => { + it('should return new HttpState with default values when passing initialState equals undefined and action equals {}', () => { + let initialState = undefined; + let action = {}; + + let finalState = httpReducer(initialState, action); + + expect(finalState).not.to.be.undefined; + expect(finalState.httpCallsInProgress).to.be.false; + expect(finalState.numberOfCalls).to.be.equal(0); + }); + + it('should return new HttpState with default values when passing initialState equals new HttpState() and action equals {}', () => { + let initialState = new HttpState(); + let action = {}; + + let finalState = httpReducer(initialState, action); + + expect(finalState).not.to.be.undefined; + expect(finalState.httpCallsInProgress).to.be.false; + expect(finalState.numberOfCalls).to.be.equal(0); + }); + + it('should return new HttpState with same values when passing initialState with HttpState.httpCallsInProgress equals false '+ + 'and action equals {}', () => { + let initialState = new HttpState(); + initialState.httpCallsInProgress = false; + let action = {}; + + deepFreeze(initialState); + let finalState = httpReducer(initialState, action); + + expect(finalState.httpCallsInProgress).to.be.false; + }); + + it('should return new HttpState with same values when passing initialState with HttpState.numberOfCalls equals 2 '+ + 'and action equals {}', () => { + let initialState = new HttpState(); + initialState.numberOfCalls = 2; + let action = {}; + + deepFreeze(initialState); + let finalState = httpReducer(initialState, action); + + expect(finalState.numberOfCalls).to.be.equal(2); + }); + + it('should return new HttpState with numberOfCalls equals 1 and httpCallsInProgress equals true when passing initialState equals new HttpState() '+ + 'and action equals { type: "HTTP_GET_CALL_STARTED" }', () => { + let initialState = new HttpState(); + let action = { + type: "HTTP_GET_CALL_STARTED" + }; + + deepFreeze(initialState); + let finalState = httpReducer(initialState, action); + + expect(finalState.numberOfCalls).to.be.equal(1); + expect(finalState.httpCallsInProgress).to.be.true; + }); + + it('should return new HttpState with numberOfCalls equals 3 and httpCallsInProgress equals true when passing initialState with '+ + 'HttpState.numberOfCalls equals 2 and action equals { type: "HTTP_GET_CALL_STARTED" }', () => { + let initialState = new HttpState(); + initialState.numberOfCalls = 2; + + let action = { + type: "HTTP_GET_CALL_STARTED" + }; + + deepFreeze(initialState); + let finalState = httpReducer(initialState, action); + + expect(finalState.numberOfCalls).to.be.equal(3); + expect(finalState.httpCallsInProgress).to.be.true; + }); + + it('should return new HttpState with numberOfCalls equals 0 and httpCallsInProgress equals false when passing initialState equals new HttpState() '+ + 'and action equals { type: "HTTP_GET_CALL_COMPLETED" }', () => { + let initialState = new HttpState(); + let action = { + type: "HTTP_GET_CALL_COMPLETED" + }; + + deepFreeze(initialState); + let finalState = httpReducer(initialState, action); + + expect(finalState.numberOfCalls).to.be.equal(0); + expect(finalState.httpCallsInProgress).to.be.false; + }); + + it('should return new HttpState with numberOfCalls equals 0 and httpCallsInProgress equals false when passing initialState with '+ + 'HttpState equals {numberOfCalls: 1, httpCallsInProgress: true} and action equals { type: "HTTP_GET_CALL_COMPLETED" }', () => { + let initialState = new HttpState(); + initialState.numberOfCalls = 1; + initialState.httpCallsInProgress = true; + + let action = { + type: "HTTP_GET_CALL_COMPLETED" + }; + + deepFreeze(initialState); + let finalState = httpReducer(initialState, action); + + expect(finalState.numberOfCalls).to.be.equal(0); + expect(finalState.httpCallsInProgress).to.be.false; + }); + + it('should return new HttpState with numberOfCalls equals 1 and httpCallsInProgress equals true when passing initialState with '+ + 'HttpState equals {numberOfCalls: 2, httpCallsInProgress: true} and action equals { type: "HTTP_GET_CALL_COMPLETED" }', () => { + let initialState = new HttpState(); + initialState.numberOfCalls = 2; + initialState.httpCallsInProgress = true; + + let action = { + type: "HTTP_GET_CALL_COMPLETED" + }; + + deepFreeze(initialState); + let finalState = httpReducer(initialState, action); + + expect(finalState.numberOfCalls).to.be.equal(1); + expect(finalState.httpCallsInProgress).to.be.true; + }); +}) diff --git a/16 Immutablejs/src/reducers/spec/memberReducer.spec.ts b/16 Immutablejs/src/reducers/spec/memberReducer.spec.ts new file mode 100644 index 0000000..c2f1d2c --- /dev/null +++ b/16 Immutablejs/src/reducers/spec/memberReducer.spec.ts @@ -0,0 +1,288 @@ +import { expect } from 'chai'; +import * as deepFreeze from 'deep-freeze'; +import { memberReducer, MemberState } from '../memberReducer'; +import MemberEntity from '../../api/memberEntity'; +import MemberFormErrors from "../../validations/memberFormErrors"; + +describe('memberReducer', () => { + it('should return new MemberState with default values when passing initialState equals undefined and action equals {}', () => { + let initialState = undefined; + let action = {}; + + let finalState = memberReducer(initialState, action); + + expect(finalState).not.to.be.undefined; + expect(finalState.member).not.to.be.undefined; + expect(finalState.member.id).to.be.equal(-1); + expect(finalState.member.login).to.be.empty; + expect(finalState.member.avatar_url).to.be.empty; + expect(finalState.memberId).to.be.equal(-1); + expect(finalState.isValid).to.be.false; + expect(finalState.saveCompleted).to.be.false; + expect(finalState.errors).not.to.be.undefined; + expect(finalState.errors.id).to.be.empty; + expect(finalState.errors.login).to.be.empty; + expect(finalState.errors.avatar_url).to.be.empty; + expect(finalState.errors.isEntityValid).to.be.undefined; + }); + + it('should return new MemberState with same values when passing initialState with MemberState.member.id equals 2 and action equals {}', () => { + let initialState = new MemberState(); + initialState.member.id = 2; + let action = {}; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.id).to.be.equal(2); + }); + + it('should return new MemberState with default values when passing initialState with MemberState.member.id equals 2 and '+ + 'action equals { type: "MEMBER_INITIALIZE_NEW" }', () => { + let initialState = new MemberState(); + initialState.member.id = 2; + + let action = { + type: "MEMBER_INITIALIZE_NEW" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.id).to.be.equal(-1); + }); + + it('should return new MemberState with isValid equals false when passing initialState with MemberState.IsValid equals true and '+ + 'action equals { type: "MEMBER_INITIALIZE_NEW" }', () => { + let initialState = new MemberState(); + initialState.isValid = true; + + let action = { + type: "MEMBER_INITIALIZE_NEW" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.isValid).to.be.false; + }); + + it('should return new MemberState with member equals action.member values when passing initialState equals new MemberState() and '+ + 'action equals { type: "MEMBER_LOAD", member: member }', () => { + let initialState = new MemberState(); + + let member = new MemberEntity(); + member.id = 3; + member.login = "test"; + + let action = { + type: "MEMBER_LOAD", + member: member + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.id).to.be.equal(3); + expect(finalState.member.login).to.be.equal("test"); + expect(finalState.errors.id).to.be.empty; + }); + + it('should return new MemberState with isValid equals true when passing initialState with MemberState.IsValid equals false and '+ + 'action equals { type: "MEMBER_LOAD", member: new MemberEntity() }', () => { + let initialState = new MemberState(); + initialState.isValid = false; + + let member = new MemberEntity(); + + let action = { + type: "MEMBER_LOAD", + member: member + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.isValid).to.be.true; + }); + + it('should return new MemberState with member.id equals 2 when passing initialState equals new MemberState() and '+ + 'action equals { type: "MEMBER_UI_INPUT", fieldName: "id", value: 2 }', () => { + let initialState = new MemberState(); + let action = { + type: "MEMBER_UI_INPUT", + fieldName: "id", + value: 2 + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.id).to.be.equal(2); + }); + + it('should return new MemberState with member.id equals 2 when passing initialState with member.id equals 1 and '+ + 'action equals { type: "MEMBER_UI_INPUT", fieldName: "id", value: 2 }', () => { + let initialState = new MemberState(); + initialState.member.id = 1; + + let action = { + type: "MEMBER_UI_INPUT", + fieldName: "id", + value: 2 + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.id).to.be.equal(2); + }); + + it('should return new MemberState with member.login equals "test" when passing initialState equals new MemberState() and '+ + 'action equals { type: "MEMBER_UI_INPUT", fieldName: "login", value: "test" }', () => { + let initialState = new MemberState(); + let action = { + type: "MEMBER_UI_INPUT", + fieldName: "login", + value: "test" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.login).to.be.equal("test"); + }); + + it('should return new MemberState with member.login equals "test" when passing initialState with member.login equals "value" and '+ + 'action equals { type: "MEMBER_UI_INPUT", fieldName: "login", value: "test" }', () => { + let initialState = new MemberState(); + initialState.member.login = "value"; + + let action = { + type: "MEMBER_UI_INPUT", + fieldName: "login", + value: "test" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.login).to.be.equal("test"); + }); + + it('should return new MemberState with member.avatar_url equals "test" when passing initialState equals new MemberState() and '+ + 'action equals { type: "MEMBER_UI_INPUT", fieldName: "avatar_url", value: "test" }', () => { + let initialState = new MemberState(); + let action = { + type: "MEMBER_UI_INPUT", + fieldName: "avatar_url", + value: "test" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.avatar_url).to.be.equal("test"); + }); + + it('should return new MemberState with member.avatar_url equals "test" when passing initialState with member.avatar_url equals "value" and '+ + 'action equals { type: "MEMBER_UI_INPUT", fieldName: "avatar_url", value: "test" }', () => { + let initialState = new MemberState(); + initialState.member.avatar_url = "value"; + + let action = { + type: "MEMBER_UI_INPUT", + fieldName: "avatar_url", + value: "test" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.member.avatar_url).to.be.equal("test"); + }); + + it('should return new MemberState with saveCompleted equals true when passing initialState equals new MemberState() and '+ + 'action equals { type: "MEMBER_SAVE", errors: { isEntityValid: true } }', () => { + let initialState = new MemberState(); + + let action = { + type: "MEMBER_SAVE", + errors: { + isEntityValid: true + } + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.saveCompleted).to.be.true; + }); + + it('should return new MemberState with isValid equals false when passing initialState equals new MemberState() and '+ + 'action equals { type: "MEMBER_SAVE", errors: { isEntityValid: false } }', () => { + let initialState = new MemberState(); + + let action = { + type: "MEMBER_SAVE", + errors: { + isEntityValid: false + } + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.isValid).to.be.false; + }); + + it('should return new MemberState with errors.login equals "testError" when passing initialState equals new MemberState() and '+ + 'action equals { type: "MEMBER_SAVE", errors: { isEntityValid: false, login: "testError" } }', () => { + let initialState = new MemberState(); + + let action = { + type: "MEMBER_SAVE", + errors: { + isEntityValid: false, + login: "testError" + } + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.isValid).to.be.false; + expect(finalState.errors.login).to.be.equal("testError"); + expect(finalState.errors.isEntityValid).to.be.false; + }); + + it('should return new MemberState with saveCompleted equals false when passing initialState with saveCompleted equals false and '+ + 'action equals { type: "MEMBER_RESET_SAVE_COMPLETED" }', () => { + let initialState = new MemberState(); + initialState.saveCompleted = false; + + let action = { + type: "MEMBER_RESET_SAVE_COMPLETED" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.saveCompleted).to.be.false; + }); + + it('should return new MemberState with saveCompleted equals false when passing initialState with saveCompleted equals true and '+ + 'action equals { type: "MEMBER_RESET_SAVE_COMPLETED" }', () => { + let initialState = new MemberState(); + initialState.saveCompleted = true; + + let action = { + type: "MEMBER_RESET_SAVE_COMPLETED" + }; + + deepFreeze(initialState); + let finalState = memberReducer(initialState, action); + + expect(finalState.saveCompleted).to.be.false; + }); +}) diff --git a/16 Immutablejs/src/reducers/spec/membersReducer.spec.ts b/16 Immutablejs/src/reducers/spec/membersReducer.spec.ts new file mode 100644 index 0000000..cd72c19 --- /dev/null +++ b/16 Immutablejs/src/reducers/spec/membersReducer.spec.ts @@ -0,0 +1,66 @@ +import { expect } from 'chai'; +import * as deepFreeze from 'deep-freeze'; +import membersReducer from '../membersReducer'; +import MemberEntity from '../../api/memberEntity'; + +describe('membersReducer', () => { + it('should return empty array state when passing initialState equals undefined and action equals {}', () => { + let initialState = undefined; + let action = {}; + + let finalState = membersReducer(initialState, action); + + expect(finalState.length).to.be.equal(0); + }); + + it('should return empty array state when passing initialState equals [] and action equals {}', () => { + let initialState = []; + let action = {}; + + // Check that state is inmutable + deepFreeze(initialState); + let finalState = membersReducer(initialState, action); + + expect(finalState.length).to.be.equal(0); + }); + + it('should return new array with same items when passing initialState equals [member1, member2] and action equals {}', () => { + let member1 = new MemberEntity(); + let member2 = new MemberEntity(); + + member1.login = "test1"; + member2.login = "test2" + + let initialState : Array = [member1, member2]; + let action = {}; + + // Check that state is inmutable + deepFreeze(initialState); + let finalState = membersReducer(initialState, action); + + expect(finalState.length).to.be.equal(2); + expect(finalState[0].login).to.be.equal("test1"); + expect(finalState[1].login).to.be.equal("test2"); + }); + + it('should return new array with items when passing initialState equals [] and action equals ' + + '{ type: "MEMBERS_ASSIGN", members: [member1, member2]}', () => { + let member1 = new MemberEntity(); + let member2 = new MemberEntity(); + + member1.login = "test1"; + member2.login = "test2" + let members : Array = [member1, member2]; + + let initialState = []; + let action = {type: 'MEMBERS_ASSIGN', members: members}; + + // Check that state is inmutable + deepFreeze(initialState); + let finalState = membersReducer(initialState, action); + + expect(finalState.length).to.be.equal(2); + expect(finalState[0].login).to.be.equal("test1"); + expect(finalState[1].login).to.be.equal("test2"); + }); +}); diff --git a/16 Immutablejs/src/reducers/spec/reposReducer.spec.ts b/16 Immutablejs/src/reducers/spec/reposReducer.spec.ts new file mode 100644 index 0000000..99a2bea --- /dev/null +++ b/16 Immutablejs/src/reducers/spec/reposReducer.spec.ts @@ -0,0 +1,63 @@ +import { expect } from 'chai'; +import * as deepFreeze from 'deep-freeze'; +import reposReducer from '../reposReducer'; +import RepoEntity from '../../api/repoEntity'; + +describe('reposReducer', () => { + it('should return empty array state when passing initialState equals undefined and action equals {}', () => { + let initialState = undefined; + let action = {}; + + let finalState = reposReducer(initialState, action); + + expect(finalState.length).to.be.equal(0); + }); + + it('should return empty array state when passing initialState equals [] and action equals {}', () => { + let initialState = []; + let action = {}; + + deepFreeze(initialState); + let finalState = reposReducer(initialState, action); + + expect(finalState.length).to.be.equal(0); + }); + + it('should return new array with same items when passing initialState equals [repo1, repo2] and action equals {}', () => { + let repo1 = new RepoEntity(); + let repo2 = new RepoEntity(); + + repo1.name = "test1"; + repo2.name = "test2" + + let initialState : Array = [repo1, repo2]; + let action = {}; + + deepFreeze(initialState); + let finalState = reposReducer(initialState, action); + + expect(finalState.length).to.be.equal(2); + expect(finalState[0].name).to.be.equal("test1"); + expect(finalState[1].name).to.be.equal("test2"); + }); + + it('should return new array with items when passing initialState equals [] and action equals ' + + '{ type: "REPOS_ASSIGN", repos: [repo1, repo2]}', () => { + let repo1 = new RepoEntity(); + let repo2 = new RepoEntity(); + + repo1.name = "test1"; + repo2.name = "test2" + let repos : Array = [repo1, repo2]; + + let initialState = []; + let action = {type: 'REPOS_ASSIGN', repos: repos}; + + deepFreeze(initialState); + let finalState = reposReducer(initialState, action); + + expect(finalState.length).to.be.equal(2); + expect(finalState[0].name).to.be.equal("test1"); + expect(finalState[1].name).to.be.equal("test2"); + }); +}) diff --git a/16 Immutablejs/src/validations/memberFormErrors.ts b/16 Immutablejs/src/validations/memberFormErrors.ts new file mode 100644 index 0000000..8f9ca6c --- /dev/null +++ b/16 Immutablejs/src/validations/memberFormErrors.ts @@ -0,0 +1,15 @@ + +export default class MemberErrors { + id: string; + login: string; + avatar_url: string; + + isEntityValid : boolean; + + public constructor() { + this.id = ""; + this.login = ""; + this.avatar_url = ""; + + } +} diff --git a/16 Immutablejs/src/validations/memberFormValidator.ts b/16 Immutablejs/src/validations/memberFormValidator.ts new file mode 100644 index 0000000..371e196 --- /dev/null +++ b/16 Immutablejs/src/validations/memberFormValidator.ts @@ -0,0 +1,19 @@ +import MemberEntity from '../api/MemberEntity' +import MemberFormErrors from './memberFormErrors' + +class memberFormValidator { + public validateMember(member : MemberEntity) : MemberFormErrors + { + let memberFormErrors : MemberFormErrors = new MemberFormErrors(); + memberFormErrors.isEntityValid = true; + + if (member.login.length < 3) { + memberFormErrors.login = 'Login must be at least 3 characters.'; + memberFormErrors.isEntityValid = false; + } + + return memberFormErrors; + } +} + +export default new memberFormValidator(); diff --git a/16 Immutablejs/test/test_index.js b/16 Immutablejs/test/test_index.js new file mode 100644 index 0000000..b2c5094 --- /dev/null +++ b/16 Immutablejs/test/test_index.js @@ -0,0 +1,5 @@ +// require all modules ending in ".spec" from the +// current directory and all subdirectories + +var testsContext = require.context("../src", true, /.spec$/); +testsContext.keys().forEach(testsContext); diff --git a/16 Immutablejs/tsconfig.json b/16 Immutablejs/tsconfig.json new file mode 100644 index 0000000..b3342ff --- /dev/null +++ b/16 Immutablejs/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": false, + "noImplicitAny": false, + "removeComments": true, + "sourceMap": true, + "jsx": "react", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noLib": false, + "preserveConstEnums": true, + "suppressImplicitAnyIndexErrors": true + }, + "compileOnSave": false, + "filesGlob": [ + "./src/**/*.tsx", + "./src/**/*.ts", + "./test/**/*.ts", + "./typings/tsd.d.ts", + "./typings-manual/tsd.d.ts", + "!./node_modules/**/*" + ], + "exclude": [ + "node_modules" + ], + "atom": { + "rewriteTsconfig": false + } +} diff --git a/16 Immutablejs/tsd.json b/16 Immutablejs/tsd.json new file mode 100644 index 0000000..7252509 --- /dev/null +++ b/16 Immutablejs/tsd.json @@ -0,0 +1,66 @@ +{ + "version": "v4", + "repo": "borisyankov/DefinitelyTyped", + "ref": "master", + "path": "typings", + "bundle": "typings/tsd.d.ts", + "installed": { + "react/react.d.ts": { + "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" + }, + "react/react-dom.d.ts": { + "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" + }, + "react-router/history.d.ts": { + "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" + }, + "react-router/react-router.d.ts": { + "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" + }, + "lodash/lodash.d.ts": { + "commit": "ab2462147a313f69e7118625cfcce21c005fa4af" + }, + "toastr/toastr.d.ts": { + "commit": "4ba57e605bd71bf7248de65aac0b4f50e3eb1c9e" + }, + "jquery/jquery.d.ts": { + "commit": "4ba57e605bd71bf7248de65aac0b4f50e3eb1c9e" + }, + "object-assign/object-assign.d.ts": { + "commit": "4ba57e605bd71bf7248de65aac0b4f50e3eb1c9e" + }, + "redux/redux.d.ts": { + "commit": "0cd5f0a1482dc3149f5446b1d885ef6ac7c57720" + }, + "react-redux/react-redux.d.ts": { + "commit": "9f0f926a12026287b5a4a229e5672c01e7549313" + }, + "redux-thunk/redux-thunk.d.ts": { + "commit": "0cd5f0a1482dc3149f5446b1d885ef6ac7c57720" + }, + "q/Q.d.ts": { + "commit": "7ff377f4bf59f6f3a6379bca83c4082895e61110" + }, + "mocha/mocha.d.ts": { + "commit": "2f47c75835b837777a85287611703d683b0aaa83" + }, + "assertion-error/assertion-error.d.ts": { + "commit": "2f47c75835b837777a85287611703d683b0aaa83" + }, + "chai/chai.d.ts": { + "commit": "2f47c75835b837777a85287611703d683b0aaa83" + }, + "deep-freeze/deep-freeze.d.ts": { + "commit": "2f47c75835b837777a85287611703d683b0aaa83" + }, + "sinon/sinon.d.ts": { + "commit": "926711691d213f76a483b36dd728e80be1257717" + }, + "enzyme/enzyme.d.ts": { + "commit": "6766ed1d0faf02ede9e2edf2e66bbb2388c825ec" + }, + "immutable/immutable.d.ts": { + "commit": "73110d92bdab2367e326f297bef3574285cba329" + } + } +} diff --git a/16 Immutablejs/typings-manual/redux-mock-store/redux-mock-store.d.ts b/16 Immutablejs/typings-manual/redux-mock-store/redux-mock-store.d.ts new file mode 100644 index 0000000..0f56f6e --- /dev/null +++ b/16 Immutablejs/typings-manual/redux-mock-store/redux-mock-store.d.ts @@ -0,0 +1,19 @@ +// Type definitions for Redux Mock Store v1.0.2 +// Project: https://github.com/arnaudbenard/redux-mock-store +// Definitions by: Braulio Díez > +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/// + +declare module "redux-mock-store" { + interface MockStore extends Redux.Store { + getState(): any; + getActions(): Array; + dispatch(action: any): any; + clearActions(): void; + subscribe(): any; + } + + function configureStore(...args: any[]) : (...args: any[]) => MockStore; + export = configureStore; +} diff --git a/16 Immutablejs/typings-manual/tsd.d.ts b/16 Immutablejs/typings-manual/tsd.d.ts new file mode 100644 index 0000000..a2a1d73 --- /dev/null +++ b/16 Immutablejs/typings-manual/tsd.d.ts @@ -0,0 +1 @@ +/// diff --git a/16 Immutablejs/webpack.config.js b/16 Immutablejs/webpack.config.js new file mode 100644 index 0000000..5f95e85 --- /dev/null +++ b/16 Immutablejs/webpack.config.js @@ -0,0 +1,80 @@ +var path = require("path"); +var webpack = require("webpack"); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); + +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + // .js is required for react imports. + // .tsx is for our app entry point. + // .ts is optional, in case you will be importing any regular ts files. + extensions: ['', '.js', '.ts', '.tsx'] + }, + + entry: [ + './index.tsx', + './css/site.css', + '../node_modules/toastr/build/toastr.css', + '../node_modules/bootstrap/dist/css/bootstrap.css' + ], + + output: { + path: path.join(basePath, "dist"), + filename: 'bundle.js' + }, + + //https://webpack.github.io/docs/webpack-dev-server.html#webpack-dev-server-cli + devServer: { + contentBase: './dist', //Content base + inline: true, //Enable watch and live reload + host: 'localhost', + port: 8080 + }, + + // http://webpack.github.io/docs/configuration.html#devtool + devtool: 'inline-source-map', + + module: { + loaders: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + loader: 'ts-loader' + }, + //Note: Doesn't exclude node_modules to load bootstrap + { + test: /\.css$/, + loader: ExtractTextPlugin.extract('style-loader','css-loader') + }, + //Loading glyphicons => https://github.com/gowravshekar/bootstrap-webpack + //Using here url-loader and file-loader + {test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff" }, + {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream" }, + {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" }, + {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml" }, + { + test: /\.(gif|jpg|png)$/, + include: path.join(basePath, "src/images"), + loader: 'url-loader?limit=100000' + } + ] + }, + + plugins:[ + //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: 'index.html', //Name of file in ./dist/ + template: 'index.html' //Name of template in ./src + }), + //Generate bundle.css => https://github.com/webpack/extract-text-webpack-plugin + new ExtractTextPlugin('bundle.css'), + //Expose jquery used by bootstrap + new webpack.ProvidePlugin({ + $: "jquery", + jQuery: "jquery" + }) + ] +}