refactor: lint, frontend tests, vuex store

This fixes the lint errors and failing frontend tests. Also, when the
user chooses another orderBy, the menu gets translated.

The refactoring moves code and complexity into the vuex store where it
can be tested separately.
This commit is contained in:
roschaefer 2019-10-18 16:08:11 +02:00
parent 7330987a69
commit 16d07bd3ba
10 changed files with 197 additions and 99 deletions

View File

@ -67,13 +67,13 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
filteredCategoryIds: 'postsFilter/filteredCategoryIds', filteredCategoryIds: 'posts/filteredCategoryIds',
}), }),
}, },
methods: { methods: {
...mapMutations({ ...mapMutations({
resetCategories: 'postsFilter/RESET_CATEGORIES', resetCategories: 'posts/RESET_CATEGORIES',
toggleCategory: 'postsFilter/TOGGLE_CATEGORY', toggleCategory: 'posts/TOGGLE_CATEGORY',
}), }),
}, },
} }

View File

@ -50,20 +50,20 @@ describe('FilterPosts.vue', () => {
describe('mount', () => { describe('mount', () => {
mutations = { mutations = {
'postsFilter/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(), 'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'postsFilter/RESET_CATEGORIES': jest.fn(), 'posts/RESET_CATEGORIES': jest.fn(),
'postsFilter/TOGGLE_CATEGORY': jest.fn(), 'posts/TOGGLE_CATEGORY': jest.fn(),
'postsFilter/TOGGLE_EMOTION': jest.fn(), 'posts/TOGGLE_EMOTION': jest.fn(),
} }
getters = { getters = {
'postsFilter/isActive': () => false, 'posts/isActive': () => false,
'auth/isModerator': () => false, 'auth/isModerator': () => false,
'auth/user': () => { 'auth/user': () => {
return { id: 'u34' } return { id: 'u34' }
}, },
'postsFilter/filteredCategoryIds': jest.fn(() => []), 'posts/filteredCategoryIds': jest.fn(() => []),
'postsFilter/filteredByUsersFollowed': jest.fn(), 'posts/filteredByUsersFollowed': jest.fn(),
'postsFilter/filteredByEmotions': jest.fn(() => []), 'posts/filteredByEmotions': jest.fn(() => []),
} }
const openFilterPosts = () => { const openFilterPosts = () => {
const store = new Vuex.Store({ mutations, getters }) const store = new Vuex.Store({ mutations, getters })
@ -94,18 +94,18 @@ describe('FilterPosts.vue', () => {
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
environmentAndNatureButton = wrapper.findAll('button').at(2) environmentAndNatureButton = wrapper.findAll('button').at(2)
environmentAndNatureButton.trigger('click') environmentAndNatureButton.trigger('click')
expect(mutations['postsFilter/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4') expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
}) })
it('sets category button attribute `primary` when corresponding category is filtered', () => { it('sets category button attribute `primary` when corresponding category is filtered', () => {
getters['postsFilter/filteredCategoryIds'] = jest.fn(() => ['cat9']) getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
democracyAndPoliticsButton = wrapper.findAll('button').at(4) democracyAndPoliticsButton = wrapper.findAll('button').at(4)
expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary') expect(democracyAndPoliticsButton.attributes().class).toContain('ds-button-primary')
}) })
it('sets "filter-by-followed-authors-only" button attribute `primary`', () => { it('sets "filter-by-followed-authors-only" button attribute `primary`', () => {
getters['postsFilter/filteredByUsersFollowed'] = jest.fn(() => true) getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
expect( expect(
wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'), wrapper.find({ name: 'filter-by-followed-authors-only' }).classes('ds-button-primary'),
@ -120,7 +120,7 @@ describe('FilterPosts.vue', () => {
}) })
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => { it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
expect(mutations['postsFilter/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34') expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
}) })
}) })
@ -129,11 +129,11 @@ describe('FilterPosts.vue', () => {
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1) happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1)
happyEmotionButton.trigger('click') happyEmotionButton.trigger('click')
expect(mutations['postsFilter/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy') expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
}) })
it('sets the attribute `src` to colorized image', () => { it('sets the attribute `src` to colorized image', () => {
getters['postsFilter/filteredByEmotions'] = jest.fn(() => ['happy']) getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
const wrapper = openFilterPosts() const wrapper = openFilterPosts()
happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1) happyEmotionButton = wrapper.findAll('button.emotions-buttons').at(1)
const happyEmotionButtonImage = happyEmotionButton.find('img') const happyEmotionButtonImage = happyEmotionButton.find('img')

View File

@ -39,7 +39,7 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'auth/user', currentUser: 'auth/user',
filterActive: 'postsFilter/isActive', filterActive: 'posts/isActive',
}), }),
chunk() { chunk() {
return chunk(this.categories, 2) return chunk(this.categories, 2)

View File

@ -68,14 +68,14 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
filteredByUsersFollowed: 'postsFilter/filteredByUsersFollowed', filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
filteredByEmotions: 'postsFilter/filteredByEmotions', filteredByEmotions: 'posts/filteredByEmotions',
}), }),
}, },
methods: { methods: {
...mapMutations({ ...mapMutations({
toggleFilteredByFollowed: 'postsFilter/TOGGLE_FILTER_BY_FOLLOWED', toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
toogleFilteredByEmotions: 'postsFilter/TOGGLE_EMOTION', toogleFilteredByEmotions: 'posts/TOGGLE_EMOTION',
}), }),
iconPath(emotion) { iconPath(emotion) {
if (this.filteredByEmotions.includes(emotion)) { if (this.filteredByEmotions.includes(emotion)) {

View File

@ -50,6 +50,18 @@
} }
} }
}, },
"store": {
"posts": {
"orderBy": {
"newest": {
"label": "Neueste"
},
"oldest": {
"label": "Älteste"
}
}
}
},
"maintenance": { "maintenance": {
"title": "Human Connection befindet sich in der Wartung", "title": "Human Connection befindet sich in der Wartung",
"explanation": "Zurzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuch es später erneut.", "explanation": "Zurzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuch es später erneut.",
@ -95,10 +107,6 @@
"code-of-conduct": "Verhaltenscodex", "code-of-conduct": "Verhaltenscodex",
"back-to-login": "Zurück zur Anmeldung" "back-to-login": "Zurück zur Anmeldung"
}, },
"sorting": {
"newest": "Neueste",
"oldest": "Älteste"
},
"login": { "login": {
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.", "copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
"login": "Einloggen", "login": "Einloggen",

View File

@ -51,6 +51,18 @@
} }
} }
}, },
"store": {
"posts": {
"orderBy": {
"newest": {
"label": "Newest"
},
"oldest": {
"label": "Oldest"
}
}
}
},
"maintenance": { "maintenance": {
"title": "Human Connection is under maintenance", "title": "Human Connection is under maintenance",
"explanation": "At the moment we are doing some scheduled maintenance, please try again later.", "explanation": "At the moment we are doing some scheduled maintenance, please try again later.",
@ -96,10 +108,6 @@
"code-of-conduct": "Code of Conduct", "code-of-conduct": "Code of Conduct",
"back-to-login": "Back to login page" "back-to-login": "Back to login page"
}, },
"sorting": {
"newest": "Newest",
"oldest": "Oldest"
},
"login": { "login": {
"copy": "If you already have a human-connection account, please login.", "copy": "If you already have a human-connection account, please login.",
"login": "Login", "login": "Login",

View File

@ -24,15 +24,37 @@ describe('PostIndex', () => {
let Wrapper let Wrapper
let store let store
let mocks let mocks
let mutations
beforeEach(() => { beforeEach(() => {
mutations = {
'posts/SELECT_ORDER': jest.fn(),
}
store = new Vuex.Store({ store = new Vuex.Store({
getters: { getters: {
'postsFilter/postsFilter': () => ({}), 'posts/filter': () => ({}),
'posts/orderOptions': () => () => [
{
key: 'store.posts.orderBy.oldest.label',
label: 'store.posts.orderBy.oldest.label',
icon: 'sort-amount-asc',
value: 'createdAt_asc',
},
{
key: 'store.posts.orderBy.newest.label',
label: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
value: 'createdAt_desc',
},
],
'posts/selectedOrder': () => () => 'createdAt_desc',
'posts/orderIcon': () => 'sort-amount-desc',
'posts/orderBy': () => 'createdAt_desc',
'auth/user': () => { 'auth/user': () => {
return { id: 'u23' } return { id: 'u23' }
}, },
}, },
mutations,
}) })
mocks = { mocks = {
$t: key => key, $t: key => key,
@ -103,12 +125,12 @@ describe('PostIndex', () => {
}) })
}) })
it('sets the post in the store when there are posts', () => { it('calls store when using order by menu', () => {
wrapper wrapper
.findAll('li') .findAll('li')
.at(0) .at(0)
.trigger('click') .trigger('click')
expect(wrapper.vm.sorting).toEqual('createdAt_desc') expect(mutations['posts/SELECT_ORDER']).toHaveBeenCalledWith({}, 'createdAt_asc')
}) })
it('updates offset when a user clicks on the load more button', () => { it('updates offset when a user clicks on the load more button', () => {

View File

@ -10,8 +10,7 @@
v-model="selected" v-model="selected"
:options="sortingOptions" :options="sortingOptions"
size="large" size="large"
v-bind:icon-right="sortingIcon" :icon-right="sortingIcon"
@input="updateOrder"
></ds-select> ></ds-select>
</div> </div>
</ds-grid-item> </ds-grid-item>
@ -83,34 +82,29 @@ export default {
offset: 0, offset: 0,
pageSize: 12, pageSize: 12,
hashtag, hashtag,
sortingIcon: 'sort-amount-desc',
selected: this.$t('sorting.newest'),
sortingOptions: [
{
label: this.$t('sorting.newest'),
value: 'Newest',
icons: 'sort-amount-desc',
order: 'createdAt_desc',
},
{
label: this.$t('sorting.oldest'),
value: 'Oldest',
icons: 'sort-amount-asc',
order: 'createdAt_asc',
},
],
}
},
mounted() {
if(this.postsOrder.label) {
this.selected = this.postsOrder.label
} }
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
postsFilter: 'postsFilter/postsFilter', postsFilter: 'posts/filter',
postsOrder: 'postsFilter/postsOrder', orderOptions: 'posts/orderOptions',
orderBy: 'posts/orderBy',
selectedOrder: 'posts/selectedOrder',
sortingIcon: 'posts/orderIcon',
}), }),
selected: {
get() {
return this.selectedOrder(this)
},
set({ value }) {
this.offset = 0
this.posts = []
this.selectOrder(value)
},
},
sortingOptions() {
return this.orderOptions(this)
},
finalFilters() { finalFilters() {
let filter = this.postsFilter let filter = this.postsFilter
if (this.hashtag) { if (this.hashtag) {
@ -127,15 +121,8 @@ export default {
}, },
methods: { methods: {
...mapMutations({ ...mapMutations({
setPostsOrder: 'postsFilter/UPDATE_ORDER', selectOrder: 'posts/SELECT_ORDER',
}), }),
updateOrder(sortingOptions) {
this.offset = 0
this.posts = []
this.sortingIcon = sortingOptions.icons
this.selected = sortingOptions.label
this.setPostsOrder(sortingOptions)
},
clearSearch() { clearSearch() {
this.$router.push({ path: '/' }) this.$router.push({ path: '/' })
this.hashtag = null this.hashtag = null
@ -156,7 +143,7 @@ export default {
offset: this.offset, offset: this.offset,
filter: this.finalFilters, filter: this.finalFilters,
first: this.pageSize, first: this.pageSize,
orderBy: this.sorting, orderBy: this.orderBy,
}, },
updateQuery: (previousResult, { fetchMoreResult }) => { updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult || fetchMoreResult.Post.length < this.pageSize) { if (!fetchMoreResult || fetchMoreResult.Post.length < this.pageSize) {
@ -184,7 +171,7 @@ export default {
return { return {
filter: this.finalFilters, filter: this.finalFilters,
first: this.pageSize, first: this.pageSize,
orderBy: this.postsOrder.order, orderBy: this.orderBy,
offset: 0, offset: 0,
} }
}, },

View File

@ -7,17 +7,25 @@ import clone from 'lodash/clone'
const defaultFilter = {} const defaultFilter = {}
const orderOptions = {
createdAt_asc: {
value: 'createdAt_asc',
key: 'store.posts.orderBy.oldest.label',
icon: 'sort-amount-asc',
},
createdAt_desc: {
value: 'createdAt_desc',
key: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
},
}
export const state = () => { export const state = () => {
return { return {
filter: { filter: {
...defaultFilter, ...defaultFilter,
}, },
postsOrder: { order: orderOptions['createdAt_desc'],
label: null,
value: 'Newest',
icons: 'sort-amount-desc',
order: 'createdAt_desc',
}
} }
} }
@ -52,16 +60,16 @@ export const mutations = {
if (isEmpty(get(filter, 'emotions_some.emotion_in'))) delete filter.emotions_some if (isEmpty(get(filter, 'emotions_some.emotion_in'))) delete filter.emotions_some
state.filter = filter state.filter = filter
}, },
UPDATE_ORDER(state, postsOrder) { SELECT_ORDER(state, value) {
state.postsOrder = postsOrder state.order = orderOptions[value]
} },
} }
export const getters = { export const getters = {
isActive(state) { isActive(state) {
return !isEqual(state.filter, defaultFilter) return !isEqual(state.filter, defaultFilter)
}, },
postsFilter(state) { filter(state) {
return state.filter return state.filter
}, },
filteredCategoryIds(state) { filteredCategoryIds(state) {
@ -73,7 +81,23 @@ export const getters = {
filteredByEmotions(state) { filteredByEmotions(state) {
return get(state.filter, 'emotions_some.emotion_in') || [] return get(state.filter, 'emotions_some.emotion_in') || []
}, },
postsOrder(state) { orderOptions: state => ({ $t }) =>
return state.postsOrder Object.values(orderOptions).map(option => {
} return {
...option,
label: $t(option.key),
}
}),
selectedOrder: state => ({ $t }) => {
return {
...state.order,
label: $t(state.order.key),
}
},
orderBy(state) {
return state.order.value
},
orderIcon(state) {
return state.order.icon
},
} }

View File

@ -1,7 +1,7 @@
import { getters, mutations } from './postsFilter.js' import { getters, mutations } from './posts.js'
let state let state
let testAction let testMutation
describe('getters', () => { describe('getters', () => {
describe('isActive', () => { describe('isActive', () => {
@ -25,10 +25,10 @@ describe('getters', () => {
}) })
}) })
describe('postsFilter', () => { describe('filter', () => {
it('returns filter', () => { it('returns filter', () => {
state = { filter: { author: { followedBy_some: { id: 7 } } } } state = { filter: { author: { followedBy_some: { id: 7 } } } }
expect(getters.postsFilter(state)).toEqual({ author: { followedBy_some: { id: 7 } } }) expect(getters.filter(state)).toEqual({ author: { followedBy_some: { id: 7 } } })
}) })
}) })
@ -67,14 +67,48 @@ describe('getters', () => {
expect(getters.filteredByEmotions(state)).toEqual([]) expect(getters.filteredByEmotions(state)).toEqual([])
}) })
}) })
describe('orderByOptions', () => {
it('returns all options regardless of current state', () => {
const $t = jest.fn(t => t)
expect(getters.orderOptions()({ $t })).toEqual([
{
key: 'store.posts.orderBy.oldest.label',
label: 'store.posts.orderBy.oldest.label',
icon: 'sort-amount-asc',
value: 'createdAt_asc',
},
{
key: 'store.posts.orderBy.newest.label',
label: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
value: 'createdAt_desc',
},
])
})
})
describe('orderBy', () => {
it('returns value for graphql query', () => {
state = {
order: {
key: 'store.posts.orderBy.newest.label',
label: 'store.posts.orderBy.newest.label',
icon: 'sort-amount-desc',
value: 'createdAt_desc',
},
}
expect(getters.orderBy(state)).toEqual('createdAt_desc')
})
})
}) })
describe('mutations', () => { describe('mutations', () => {
describe('RESET_CATEGORIES', () => { describe('RESET_CATEGORIES', () => {
beforeEach(() => { beforeEach(() => {
testAction = categoryId => { testMutation = categoryId => {
mutations.RESET_CATEGORIES(state, categoryId) mutations.RESET_CATEGORIES(state, categoryId)
return getters.postsFilter(state) return getters.filter(state)
} }
}) })
it('resets the categories filter', () => { it('resets the categories filter', () => {
@ -84,37 +118,37 @@ describe('mutations', () => {
categories_some: { id_in: [23] }, categories_some: { id_in: [23] },
}, },
} }
expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } }) expect(testMutation(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
}) })
}) })
describe('TOGGLE_CATEGORY', () => { describe('TOGGLE_CATEGORY', () => {
beforeEach(() => { beforeEach(() => {
testAction = categoryId => { testMutation = categoryId => {
mutations.TOGGLE_CATEGORY(state, categoryId) mutations.TOGGLE_CATEGORY(state, categoryId)
return getters.postsFilter(state) return getters.filter(state)
} }
}) })
it('creates category filter if empty', () => { it('creates category filter if empty', () => {
state = { filter: {} } state = { filter: {} }
expect(testAction(23)).toEqual({ categories_some: { id_in: [23] } }) expect(testMutation(23)).toEqual({ categories_some: { id_in: [23] } })
}) })
it('adds category id not present', () => { it('adds category id not present', () => {
state = { filter: { categories_some: { id_in: [24] } } } state = { filter: { categories_some: { id_in: [24] } } }
expect(testAction(23)).toEqual({ categories_some: { id_in: [24, 23] } }) expect(testMutation(23)).toEqual({ categories_some: { id_in: [24, 23] } })
}) })
it('removes category id if present', () => { it('removes category id if present', () => {
state = { filter: { categories_some: { id_in: [23, 24] } } } state = { filter: { categories_some: { id_in: [23, 24] } } }
const result = testAction(23) const result = testMutation(23)
expect(result).toEqual({ categories_some: { id_in: [24] } }) expect(result).toEqual({ categories_some: { id_in: [24] } })
}) })
it('removes category filter if empty', () => { it('removes category filter if empty', () => {
state = { filter: { categories_some: { id_in: [23] } } } state = { filter: { categories_some: { id_in: [23] } } }
expect(testAction(23)).toEqual({}) expect(testMutation(23)).toEqual({})
}) })
it('does not get in the way of other filters', () => { it('does not get in the way of other filters', () => {
@ -124,15 +158,15 @@ describe('mutations', () => {
categories_some: { id_in: [23] }, categories_some: { id_in: [23] },
}, },
} }
expect(testAction(23)).toEqual({ author: { followedBy_some: { id: 7 } } }) expect(testMutation(23)).toEqual({ author: { followedBy_some: { id: 7 } } })
}) })
}) })
describe('TOGGLE_FILTER_BY_FOLLOWED', () => { describe('TOGGLE_FILTER_BY_FOLLOWED', () => {
beforeEach(() => { beforeEach(() => {
testAction = userId => { testMutation = userId => {
mutations.TOGGLE_FILTER_BY_FOLLOWED(state, userId) mutations.TOGGLE_FILTER_BY_FOLLOWED(state, userId)
return getters.postsFilter(state) return getters.filter(state)
} }
}) })
@ -142,7 +176,7 @@ describe('mutations', () => {
}) })
it('attaches the id of the current user to the filter object', () => { it('attaches the id of the current user to the filter object', () => {
expect(testAction(4711)).toEqual({ author: { followedBy_some: { id: 4711 } } }) expect(testMutation(4711)).toEqual({ author: { followedBy_some: { id: 4711 } } })
}) })
}) })
@ -152,8 +186,23 @@ describe('mutations', () => {
}) })
it('remove the id of the current user from the filter object', () => { it('remove the id of the current user from the filter object', () => {
expect(testAction(4711)).toEqual({}) expect(testMutation(4711)).toEqual({})
}) })
}) })
}) })
describe('SELECT_ORDER', () => {
beforeEach(() => {
testMutation = key => {
mutations.SELECT_ORDER(state, key)
return getters.orderBy(state)
}
})
it('switches the currently selected order', () => {
state = {
// does not matter
}
expect(testMutation('createdAt_asc')).toEqual('createdAt_asc')
})
})
}) })