From d09e45b0a86911be767a07b8acdd175ce34973f6 Mon Sep 17 00:00:00 2001 From: DmitryTsepelev Date: Tue, 10 Jul 2018 13:10:16 +0300 Subject: [PATCH] UserInfo uses redux store --- .../components/thread/thread.getters.test.js | 31 +++++ .../components/thread/thread.reducers.test.js | 23 +--- web/app/components/user-info/index.js | 4 + .../components/user-info/user-info.actions.js | 12 ++ .../components/user-info/user-info.getters.js | 2 + .../user-info/user-info.getters.test.js | 40 ++++++ web/app/components/user-info/user-info.jsx | 42 +++--- .../user-info/user-info.reducers.js | 37 +++++ .../user-info/user-info.reducers.test.js | 45 ++++++ web/app/remark.js | 18 +-- web/app/store.js | 2 + web/package.json | 1 + web/yarn-error.log | 128 ------------------ 13 files changed, 216 insertions(+), 169 deletions(-) create mode 100644 web/app/components/thread/thread.getters.test.js create mode 100644 web/app/components/user-info/user-info.actions.js create mode 100644 web/app/components/user-info/user-info.getters.js create mode 100644 web/app/components/user-info/user-info.getters.test.js create mode 100644 web/app/components/user-info/user-info.reducers.js create mode 100644 web/app/components/user-info/user-info.reducers.test.js delete mode 100644 web/yarn-error.log diff --git a/web/app/components/thread/thread.getters.test.js b/web/app/components/thread/thread.getters.test.js new file mode 100644 index 00000000..e8fc6791 --- /dev/null +++ b/web/app/components/thread/thread.getters.test.js @@ -0,0 +1,31 @@ +import store from 'common/store'; +import { getThreadIsCollapsed } from './thread.getters'; + +describe('collapsedThreads', () => { + const notScoredComment = { id: 1 }; + const goodComment = { id: 1, score: 3 }; + const badComment = { id: 1, score: -1 }; + + it('takes value from the state', () => { + const state = { collapsedThreads: { [notScoredComment.id]: true } }; + const collapsed = getThreadIsCollapsed(state, notScoredComment); + expect(collapsed).toEqual(true); + }); + + beforeAll(() => { + const config = { critical_score: 2 }; + store.set('config', config); + }); + + it('returns true when score is less then critical_score', () => { + const state = { collapsedThreads: {} }; + const collapsed = getThreadIsCollapsed(state, badComment); + expect(collapsed).toEqual(true); + }); + + it('returns true when score is less then critical_score', () => { + const state = { collapsedThreads: {} }; + const collapsed = getThreadIsCollapsed(state, goodComment); + expect(collapsed).toEqual(false); + }); +}); diff --git a/web/app/components/thread/thread.reducers.test.js b/web/app/components/thread/thread.reducers.test.js index f48e8931..954feaee 100644 --- a/web/app/components/thread/thread.reducers.test.js +++ b/web/app/components/thread/thread.reducers.test.js @@ -1,29 +1,18 @@ import { setCollapse } from './thread.actions'; import { collapsedThreads } from './thread.reducers'; -import { getThreadIsCollapsed } from './thread.getters'; describe('collapsedThreads', () => { const comment = { id: 1 }; it('should set collapsed to true', () => { - const collapsed = true; - const action = setCollapse(comment, collapsed); - - const newState = { - collapsedThreads: collapsedThreads({}, action), - }; - - expect(getThreadIsCollapsed(newState, comment)).toEqual(collapsed); + const action = setCollapse(comment, true); + const newState = collapsedThreads({}, action); + expect(newState).toEqual({ [comment.id]: true }); }); it('should set collapsed to false', () => { - const collapsed = false; - const action = setCollapse(comment, collapsed); - - const newState = { - collapsedThreads: collapsedThreads({}, action), - }; - - expect(getThreadIsCollapsed(newState, comment)).toEqual(collapsed); + const action = setCollapse(comment, false); + const newState = collapsedThreads({}, action); + expect(newState).toEqual({ [comment.id]: false }); }); }); diff --git a/web/app/components/user-info/index.js b/web/app/components/user-info/index.js index 6f9f4544..142fba6e 100644 --- a/web/app/components/user-info/index.js +++ b/web/app/components/user-info/index.js @@ -1,3 +1,7 @@ +import { userComments, isLoadingUserComments } from './user-info.reducers'; + +export const userInfoReducers = { userComments, isLoadingUserComments }; + export { default } from './user-info'; require('./user-info.scss'); diff --git a/web/app/components/user-info/user-info.actions.js b/web/app/components/user-info/user-info.actions.js new file mode 100644 index 00000000..403ca402 --- /dev/null +++ b/web/app/components/user-info/user-info.actions.js @@ -0,0 +1,12 @@ +export const USER_INFO_FETCH_COMMENTS = 'USER_INFO/FETCH_COMMENTS'; +export const fetchComments = userId => ({ + type: USER_INFO_FETCH_COMMENTS, + userId, +}); + +export const USER_INFO_COMPLETE_FETCH_COMMENTS = 'USER_INFO/COMPLETE_FETCH_COMMENTS'; +export const completeFetchComments = (userId, comments) => ({ + type: USER_INFO_COMPLETE_FETCH_COMMENTS, + userId, + comments, +}); diff --git a/web/app/components/user-info/user-info.getters.js b/web/app/components/user-info/user-info.getters.js new file mode 100644 index 00000000..c5067547 --- /dev/null +++ b/web/app/components/user-info/user-info.getters.js @@ -0,0 +1,2 @@ +export const getUserComments = (state, userId) => state.userComments[userId] || null; +export const getIsLoadingUserComments = (state, userId) => state.isLoadingUserComments[userId] || false; diff --git a/web/app/components/user-info/user-info.getters.test.js b/web/app/components/user-info/user-info.getters.test.js new file mode 100644 index 00000000..c59118ac --- /dev/null +++ b/web/app/components/user-info/user-info.getters.test.js @@ -0,0 +1,40 @@ +import { getUserComments, getIsLoadingUserComments } from './user-info.getters'; + +describe('getUserComments', () => { + const userId = 1; + const comments = [{ id: 1 }]; + + it('returns null when comments have not been loaded', () => { + const state = { userComments: {} }; + const userComments = getUserComments(state, userId); + expect(userComments).toEqual(null); + }); + + it('returns comments when comments have been loaded', () => { + const state = { userComments: { [userId]: comments } }; + const userComments = getUserComments(state, userId); + expect(userComments).toEqual(comments); + }); +}); + +describe('getIsLoadingUserComments', () => { + const userId = 1; + + it('returns false when state is empty', () => { + const state = { isLoadingUserComments: {} }; + const loading = getIsLoadingUserComments(state, userId); + expect(loading).toEqual(false); + }); + + it('returns false when comments are not loading', () => { + const state = { isLoadingUserComments: { [userId]: false } }; + const loading = getIsLoadingUserComments(state, userId); + expect(loading).toEqual(false); + }); + + it('returns true when comments are loading', () => { + const state = { isLoadingUserComments: { [userId]: true } }; + const loading = getIsLoadingUserComments(state, userId); + expect(loading).toEqual(true); + }); +}); diff --git a/web/app/components/user-info/user-info.jsx b/web/app/components/user-info/user-info.jsx index 2959ecf1..1e91ffde 100644 --- a/web/app/components/user-info/user-info.jsx +++ b/web/app/components/user-info/user-info.jsx @@ -1,36 +1,40 @@ /** @jsx h */ import { h, Component } from 'preact'; +import { connect } from 'preact-redux'; import api from 'common/api'; import { getHandleClickProps } from 'common/accessibility'; import LastCommentsList from './last-comments-list'; import Avatar from 'components/avatar-icon'; +import { fetchComments, completeFetchComments } from './user-info.actions'; +import { getUserComments, getIsLoadingUserComments } from './user-info.getters'; class UserInfo extends Component { - constructor(props) { - super(props); - - this.state = { - comments: [], - isLoading: true, - }; - } - componentWillMount() { const { user: { id }, + comments, + isLoading, + fetchComments, + completeFetchComments, } = this.props; - api - .getUserComments({ user: id, limit: 10 }) - .then(({ comments = [] }) => this.setState({ comments })) - .finally(() => this.setState({ isLoading: false })); + if (!comments && !isLoading) { + fetchComments(id); + + api + .getUserComments({ user: id, limit: 10 }) + .then(({ comments }) => completeFetchComments(id, comments)) + .catch(() => completeFetchComments(id, [])); + } } - render(props, { comments, isLoading }) { + render(props) { const { user: { name, id, isDefaultPicture, picture }, + comments = [], + isLoading, onClose, } = props; @@ -40,7 +44,7 @@ class UserInfo extends Component {

Last comments by {name}

{id}

- + {!!comments && } Close ✖ @@ -50,4 +54,10 @@ class UserInfo extends Component { } } -export default UserInfo; +export default connect( + (state, props) => ({ + comments: getUserComments(state, props.user.id), + isLoading: getIsLoadingUserComments(state, props.user.id), + }), + { fetchComments, completeFetchComments } +)(UserInfo); diff --git a/web/app/components/user-info/user-info.reducers.js b/web/app/components/user-info/user-info.reducers.js new file mode 100644 index 00000000..23e58b7d --- /dev/null +++ b/web/app/components/user-info/user-info.reducers.js @@ -0,0 +1,37 @@ +import { USER_INFO_FETCH_COMMENTS, USER_INFO_COMPLETE_FETCH_COMMENTS } from './user-info.actions'; + +export const userComments = (state = {}, action) => { + switch (action.type) { + case USER_INFO_FETCH_COMMENTS: { + return { + ...state, + [action.userId]: [], + }; + } + case USER_INFO_COMPLETE_FETCH_COMMENTS: + return { + ...state, + [action.userId]: action.comments, + }; + default: + return state; + } +}; + +export const isLoadingUserComments = (state = {}, action) => { + switch (action.type) { + case USER_INFO_FETCH_COMMENTS: { + return { + ...state, + [action.userId]: true, + }; + } + case USER_INFO_COMPLETE_FETCH_COMMENTS: + return { + ...state, + [action.userId]: false, + }; + default: + return state; + } +}; diff --git a/web/app/components/user-info/user-info.reducers.test.js b/web/app/components/user-info/user-info.reducers.test.js new file mode 100644 index 00000000..12d6c8b4 --- /dev/null +++ b/web/app/components/user-info/user-info.reducers.test.js @@ -0,0 +1,45 @@ +import { fetchComments, completeFetchComments } from './user-info.actions'; +import { userComments, isLoadingUserComments } from './user-info.reducers'; + +const userId = 1; +const comments = [{ id: 1 }]; + +describe('userComments', () => { + it('should return {} by default', () => { + const action = { type: 'OTHER' }; + const newState = userComments({}, action); + expect(newState).toEqual({}); + }); + + it('should return [] on USER_INFO_FETCH_COMMENTS', () => { + const action = fetchComments(userId); + const newState = userComments({}, action); + expect(newState).toEqual({ [userId]: [] }); + }); + + it('should return comments on USER_INFO_COMPLETE_FETCH_COMMENTS', () => { + const action = completeFetchComments(userId, comments); + const newState = userComments({}, action); + expect(newState).toEqual({ [userId]: comments }); + }); +}); + +describe('isLoadingUserComments', () => { + it('should return {} by default', () => { + const action = { type: 'OTHER' }; + const newState = isLoadingUserComments({}, action); + expect(newState).toEqual({}); + }); + + it('should return [] on USER_INFO_FETCH_COMMENTS', () => { + const action = fetchComments(userId); + const newState = isLoadingUserComments({}, action); + expect(newState).toEqual({ [userId]: true }); + }); + + it('should return comments on USER_INFO_COMPLETE_FETCH_COMMENTS', () => { + const action = completeFetchComments(userId, comments); + const newState = isLoadingUserComments({}, action); + expect(newState).toEqual({ [userId]: false }); + }); +}); diff --git a/web/app/remark.js b/web/app/remark.js index c8bc6a27..8fa7b05c 100644 --- a/web/app/remark.js +++ b/web/app/remark.js @@ -22,12 +22,6 @@ loadPolyfills().then(() => { } }); -const Main = () => ( - - - -); - function init() { const node = document.getElementById(NODE_ID); @@ -63,13 +57,21 @@ function init() { render(
- + + +
, node.parentElement, node ); } else { - render(
, node.parentElement, node); + render( + + + , + node.parentElement, + node + ); } } diff --git a/web/app/store.js b/web/app/store.js index 1604ef3b..fd570b1c 100644 --- a/web/app/store.js +++ b/web/app/store.js @@ -2,9 +2,11 @@ import { createStore, applyMiddleware } from 'redux'; import { combineReducers } from 'redux'; import { threadReducers, threadMiddlewares } from './components/thread'; +import { userInfoReducers } from './components/user-info'; const reducers = combineReducers({ ...threadReducers, + ...userInfoReducers, }); const middlewares = applyMiddleware(...threadMiddlewares); diff --git a/web/package.json b/web/package.json index 8a8b0255..4bedf734 100644 --- a/web/package.json +++ b/web/package.json @@ -6,6 +6,7 @@ "start": "webpack-dev-server --progress --hot --inline --config ./webpack.config.js", "lint": "eslint --ext=.js,.jsx .", "test": "jest", + "test:coverage": "jest --coverage", "prettier": "prettier --write \"./**/*.{js,jsx,scss}\"", "precommit": "./node_modules/.bin/lint-staged" }, diff --git a/web/yarn-error.log b/web/yarn-error.log deleted file mode 100644 index 2e432a7f..00000000 --- a/web/yarn-error.log +++ /dev/null @@ -1,128 +0,0 @@ -Arguments: - /Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/bin/node /Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/bin/yarn start - -PATH: - /anaconda2/bin:/Users/dmitrytsepelev/.rvm/gems/ruby-2.5.1/bin:/Users/dmitrytsepelev/.rvm/gems/ruby-2.5.1@global/bin:/Users/dmitrytsepelev/.rvm/rubies/ruby-2.5.1/bin:/usr/local/heroku/bin:/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/bin:./bin:bin:/Users/dmitrytsepelev/bin:/usr/local/bin:/usr/local/sbin:/usr/local/share/npm/bin:/usr/bin:/bin:/usr/sbin:/sbin::/Users/dmitrytsepelev/.rvm/bin:/usr/local/go/bin:/Users/dmitrytsepelev/go/bin:/Users/dmitrytsepelev/Library/Android/sdk/tools:/Users/dmitrytsepelev/Library/Android/sdk/platform-tools:/Users/dmitrytsepelev/Library/Android/sdk/tools/proguard/bin - -Yarn version: - 1.5.1 - -Node version: - 9.2.0 - -Platform: - darwin x64 - -npm manifest: - { - "name": "remark-ui", - "version": "0.1.0", - "scripts": { - "build": "cross-env NODE_ENV=production webpack --config ./webpack.config.js", - "start": "webpack-dev-server --progress --hot --inline --config ./webpack.config.js", - "lint": "eslint --ext=.js,.jsx .", - "test": "jest", - "prettier": "prettier --write \"./**/*.{js,jsx,scss}\"", - "precommit": "./node_modules/.bin/lint-staged" - }, - "lint-staged": { - "./**/*.{js,jsx}": [ - "eslint --fix", - "git add" - ], - "./**/*.scss": [ - "prettier --write", - "git add" - ] - }, - "devDependencies": { - "autoprefixer": "^7.2.6", - "babel-core": "^6.26.3", - "babel-eslint": "^8.2.5", - "babel-loader": "^7.1.4", - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-plugin-transform-react-jsx": "^6.24.1", - "babel-preset-env": "^1.7.0", - "clean-webpack-plugin": "^0.1.19", - "copy-webpack-plugin": "^4.5.1", - "core-js": "^2.5.7", - "cross-env": "^5.2.0", - "css-loader": "^0.28.11", - "eslint": "^4.19.1", - "eslint-config-prettier": "^2.9.0", - "eslint-plugin-jsx-a11y": "^6.1.0", - "eslint-plugin-prettier": "^2.6.1", - "eslint-plugin-react": "^7.10.0", - "extract-text-webpack-plugin": "^3.0.2", - "file-loader": "^0.11.1", - "html-webpack-plugin": "^2.30.1", - "husky": "^0.14.3", - "jest": "^23.1.0", - "jest-localstorage-mock": "^2.2.0", - "lint-staged": "^7.2.0", - "postcss-calc": "^6.0.1", - "postcss-csso": "^2.0.0", - "postcss-for": "^2.1.1", - "postcss-loader": "^2.1.5", - "postcss-nested": "^3.0.0", - "postcss-simple-vars": "^4.1.0", - "postcss-url": "^6.3.1", - "postcss-wrap": "0.0.4", - "preact-redux": "^2.0.3", - "prettier": "^1.13.7", - "redux": "^4.0.0", - "style-loader": "^0.19.1", - "webpack": "^3.12.0", - "webpack-bundle-analyzer": "^2.13.1", - "webpack-dev-server": "^2.7.1" - }, - "dependencies": { - "axios": "^0.18.0", - "bem-react-helper": "^1.1.2", - "preact": "^8.2.9" - }, - "eslintIgnore": [ - "public" - ], - "jest": { - "transform": { - "^.+\\.jsx?$": "/fileTransformer.js" - }, - "setupFiles": [ - "/injectGlobalVariable.js", - "jest-localstorage-mock" - ], - "moduleDirectories": [ - "node_modules", - "/app" - ], - "testMatch": [ - "/**/*.test.js" - ] - }, - "engines": { - "node": ">=8" - } - } - -yarn manifest: - No manifest - -Lockfile: - No lockfile - -Trace: - Error: Command failed. - Exit code: 1 - Command: sh - Arguments: -c webpack-dev-server --progress --hot --inline --config ./webpack.config.js - Directory: /Users/dmitrytsepelev/dev/remark/web - Output: - - at ProcessTermError.MessageError (/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/lib/node_modules/yarn/lib/cli.js:186:110) - at new ProcessTermError (/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/lib/node_modules/yarn/lib/cli.js:226:113) - at ChildProcess. (/Users/dmitrytsepelev/.nvm/versions/node/v9.2.0/lib/node_modules/yarn/lib/cli.js:30281:17) - at ChildProcess.emit (events.js:159:13) - at maybeClose (internal/child_process.js:943:16) - at Process.ChildProcess._handle.onexit (internal/child_process.js:220:5)