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 ee5e4016..d58b6c8c 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 d31833ec..155f8863 100644 --- a/web/app/components/user-info/user-info.jsx +++ b/web/app/components/user-info/user-info.jsx @@ -1,29 +1,31 @@ /** @jsx h */ import { h, Component } from 'preact'; +import { connect } from 'preact-redux'; import api from 'common/api'; 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, [])); + } document.addEventListener('keydown', this.globalOnKeyDown); } @@ -40,9 +42,12 @@ class UserInfo extends Component { } } - render(props, { comments, isLoading }) { + render(props) { const { user: { name, id, isDefaultPicture, picture }, + comments = [], + isLoading, + onClose, } = props; return ( @@ -51,10 +56,20 @@ class UserInfo extends Component {

Last comments by {name}

{id}

- + {!!comments && } + + + Close ✖ + ); } } -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 a402e81e..840844d8 100644 --- a/web/app/remark.js +++ b/web/app/remark.js @@ -23,12 +23,6 @@ loadPolyfills().then(() => { } }); -const Main = () => ( - - - -); - function init() { const node = document.getElementById(NODE_ID); @@ -59,13 +53,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 f04f5c52..bb739a1a 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" },