Refactor: move following/followedBy updating to page component

This commit is contained in:
Raphael Beer 2020-03-28 03:26:35 +01:00
parent f69e7e42de
commit e6361c9b74
No known key found for this signature in database
GPG Key ID: C1AC5E018B25EF11
5 changed files with 142 additions and 192 deletions

View File

@ -10,29 +10,50 @@ config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['ds-space'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
const user = {
...helpers.fakeUser()[0],
followedByCount: 12,
followingCount: 15,
followedBy: helpers.fakeUser(7),
following: helpers.fakeUser(7),
}
const allConnectionsUser = {
...user,
followedBy: [
...user.followedBy,
...helpers.fakeUser(user.followedByCount - user.followedBy.length),
],
following: [...user.following, ...helpers.fakeUser(user.followingCount - user.following.length)],
}
const noConnectionsUser = {
...user,
followedByCount: 0,
followingCount: 0,
followedBy: [],
following: [],
}
describe('FollowList.vue', () => {
let store, mocks, getters, propsData
let store, getters
const Wrapper = (customProps) =>
mount(FollowList, {
store,
propsData: { user, ...customProps },
mocks: {
$t: jest.fn((str) => str),
},
localVue,
})
beforeAll(() => {
mocks = {
$t: jest.fn(),
}
getters = {
'auth/user': () => {
return {}
},
'auth/isModerator': () => false,
}
const [_user] = helpers.fakeUser()
propsData = {
user: {
..._user,
followedByCount: 12,
followingCount: 15,
followedBy: helpers.fakeUser(7),
following: helpers.fakeUser(7),
},
}
})
describe('mount', () => {
@ -42,88 +63,71 @@ describe('FollowList.vue', () => {
})
})
describe('given a user with connections', () => {
;['following', 'followedBy'].forEach((type) =>
describe(`and type=${type}`, () => {
let wrapper
let queryMock
describe('given a user', () => {
describe('without connections', () => {
it('displays the followingNobody message', () => {
const wrapper = Wrapper({ user: noConnectionsUser })
expect(wrapper.find('.no-connections').text()).toBe(
`${noConnectionsUser.name} ${wrapper.vm.$t(`profile.network.followingNobody`)}`,
)
})
beforeAll(() => {
queryMock = jest.fn().mockResolvedValue({
data: { User: [{ [type]: additionalConnections[type] }] },
})
it('displays the followedByNobody message', () => {
const wrapper = Wrapper({ user: noConnectionsUser, type: 'followedBy' })
expect(wrapper.find('.no-connections').text()).toBe(
`${noConnectionsUser.name} ${wrapper.vm.$t(`profile.network.followedByNobody`)}`,
)
})
})
wrapper = mount(FollowList, {
store,
propsData: { ...propsData, type: type },
mocks: {
...mocks,
$apollo: {
query: queryMock,
},
},
localVue,
})
})
describe('with up to 7 loaded connections', () => {
let followingWrapper
let followedByWrapper
beforeAll(() => {
followingWrapper = Wrapper()
followedByWrapper = Wrapper({ type: 'followedBy' })
})
it(`shows the users ${type}`, () => {
expect(wrapper.findAll('.user-teaser').length).toEqual(propsData.user[type].length)
})
it(`renders the connections`, () => {
expect(followedByWrapper.findAll('.user-teaser').length).toEqual(user.followedBy.length)
expect(followingWrapper.findAll('.user-teaser').length).toEqual(user.following.length)
})
it(`has a button to load all remaining users ${type}`, async () => {
jest.useFakeTimers()
it(`has a button to load all remaining connections`, async () => {
followingWrapper.find('.base-button').trigger('click')
followedByWrapper.find('.base-button').trigger('click')
expect(followingWrapper.emitted('fetchAllConnections')).toBeTruthy()
expect(followedByWrapper.emitted('fetchAllConnections')).toBeTruthy()
})
})
wrapper.find('.base-button').trigger('click')
await jest.runAllTicks()
await wrapper.vm.$nextTick()
describe('with more than 7 loaded connections', () => {
let followingWrapper
let followedByWrapper
beforeAll(() => {
followingWrapper = Wrapper({ user: allConnectionsUser })
followedByWrapper = Wrapper({ user: allConnectionsUser, type: 'followedBy' })
})
expect(wrapper.vm.connections.length).toBe(propsData.user[`${type}Count`])
expect(queryMock).toHaveBeenCalledWith({
query: wrapper.vm.queries[type],
variables: { id: propsData.user.id },
})
})
}),
)
})
it('renders the connections', () => {
expect(followedByWrapper.findAll('.user-teaser')).toHaveLength(
allConnectionsUser.followedByCount,
)
expect(followingWrapper.findAll('.user-teaser')).toHaveLength(
allConnectionsUser.followingCount,
)
})
describe('given a user without connections', () => {
;['following', 'followedBy'].forEach((type) =>
describe(`and type=${type}`, () => {
let wrapper
it('renders the user-teaser in an overflow-container', () => {
expect(followingWrapper.find('.overflow-container').is('div')).toBe(true)
expect(followedByWrapper.find('.overflow-container').is('div')).toBe(true)
})
beforeAll(() => {
wrapper = mount(FollowList, {
store,
mocks: {
$t: jest.fn().mockReturnValue('has no connections'),
},
localVue,
propsData: {
user: {
...propsData.user,
followedByCount: 0,
followingCount: 0,
followedBy: [],
following: [],
},
type,
},
})
})
it('displays the no-follower message', () => {
expect(wrapper.find('.no-connections').text()).toBe(
`${propsData.user.name} ${wrapper.vm.$t()}`,
)
})
}),
)
it('renders a filter text input', () => {
expect(followingWrapper.find('[name="followingFilter"]').is('input')).toBe(true)
expect(followedByWrapper.find('[name="followedByFilter"]').is('input')).toBe(true)
})
})
})
})
})
const additionalConnections = {
followedBy: helpers.fakeUser(5),
following: helpers.fakeUser(8),
}

View File

@ -1,7 +1,6 @@
import Vue from 'vue'
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import apolloStorybookDecorator from 'apollo-storybook-vue'
import { action } from '@storybook/addon-actions'
import helpers from '~/storybook/helpers'
import FollowList from './FollowList.vue'
@ -20,38 +19,9 @@ const allConnectionsUser = {
followedBy: [...sevenConnectionsUser.followedBy, ...helpers.fakeUser(5)],
}
const mocks = {
Query: () => ({
User: () => [allConnectionsUser],
}),
}
const typeDefs = `
type User {
followedByCount: Int
followedBy(offset: Int): [User]
name: String
slug: String
id: String
}
type Query {
User(id: ID!): [User]
}
schema {
query: Query
}
`
storiesOf('FollowList', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.addDecorator(
apolloStorybookDecorator({
mocks,
typeDefs,
Vue,
}),
)
.add('without connections', () => {
const user = {
...sevenConnectionsUser,
@ -77,7 +47,13 @@ storiesOf('FollowList', module)
user,
}
},
template: `<follow-list :user="user" type="followedBy" />`,
methods: {
fetchAllConnections(type) {
this.user = allConnectionsUser
action('fetchAllConnections')(type, this.user)
},
},
template: `<follow-list :user="user" type="followedBy" @fetchAllConnections="fetchAllConnections"/>`,
}
})

View File

@ -9,14 +9,19 @@
<p class="no-connections">{{ userName }} {{ $t(`profile.network.${type}Nobody`) }}</p>
</template>
<template v-if="connections && connections.length <= 7">
<ds-space v-for="follow in uniq(connections)" :key="follow.id" margin="x-small">
<ds-space v-for="connection in connections" :key="connection.id" margin="x-small">
<!-- TODO: find better solution for rendering errors -->
<client-only>
<user-teaser :user="follow" />
<user-teaser :user="connection" />
</client-only>
</ds-space>
<ds-space v-if="allConnectionsCount - connections.length" margin="small">
<base-button @click="fetchConnections" :loading="isLoading" size="small" color="softer">
<base-button
@click="$emit('fetchAllConnections', type)"
:loading="loading"
size="small"
color="softer"
>
{{
$t('profile.network.andMore', {
number: allConnectionsCount - connections.length,
@ -27,13 +32,9 @@
</template>
<template v-else-if="connections.length > 7">
<div class="overflow-container">
<ds-space
v-for="follow in uniq(filteredConnections)"
:key="follow.id"
margin="x-small"
>
<ds-space v-for="connection in filteredConnections" :key="connection.id" margin="x-small">
<client-only>
<user-teaser :user="follow" />
<user-teaser :user="connection" />
</client-only>
</ds-space>
</div>
@ -44,6 +45,7 @@
v-focus="true"
size="small"
icon="filter"
:name="`${type}Filter`"
/>
</ds-space>
</template>
@ -51,9 +53,7 @@
</template>
<script>
import uniqBy from 'lodash/uniqBy'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import { followedByQuery, followingQuery } from '~/graphql/User'
export default {
name: 'FollowerList',
@ -63,16 +63,11 @@ export default {
props: {
user: { type: Object, default: null },
type: { type: String, default: 'following' },
loading: { type: Boolean, default: false },
},
data() {
return {
additionalConnections: [],
filter: null,
isLoading: false,
queries: {
followedBy: followedByQuery,
following: followingQuery,
},
}
},
computed: {
@ -80,8 +75,11 @@ export default {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
},
allConnectionsCount() {
return this.user[`${this.type}Count`]
},
connections() {
return [...this.user[this.type], ...this.additionalConnections]
return this.user[this.type]
},
filteredConnections() {
if (!this.filter) {
@ -107,26 +105,10 @@ export default {
return fuzzyScores.map((score) => score.user)
},
allConnectionsCount() {
return this.user[`${this.type}Count`]
},
},
methods: {
uniq(items, field = 'id') {
return uniqBy(items, field)
},
async fetchConnections() {
this.$set(this, 'isLoading', true)
const { data } = await this.$apollo.query({
query: this.queries[this.type],
variables: { id: this.user.id },
// neither result nor update are being called when defined here (?)
})
this.additionalConnections = data.User[0][this.type]
this.$set(this, 'isLoading', false)
},
setFilter(evt) {
this.$set(this, 'filter', evt.target.value)
this.filter = evt.target.value
},
},
}

View File

@ -14,7 +14,7 @@ export default (i18n) => {
${userCountsFragment}
${locationAndBadgesFragment(lang)}
query User($id: ID!) {
query User($id: ID!, $followedByCount: Int, $followingCount: Int) {
User(id: $id) {
...user
...userCounts
@ -26,12 +26,12 @@ export default (i18n) => {
isMuted
isBlocked
blocked
following(first: 7) {
following(first: $followingCount) {
...user
...userCounts
...locationAndBadges
}
followedBy(first: 7) {
followedBy(first: $followedByCount) {
...user
...userCounts
...locationAndBadges
@ -283,27 +283,3 @@ export const currentUserQuery = gql`
}
}
`
export const followedByQuery = gql`
query($id: ID!) {
User(id: $id) {
followedBy(offset: 7) {
id
slug
name
}
}
}
`
export const followingQuery = gql`
query($id: ID!) {
User(id: $id) {
following(offset: 7) {
id
slug
name
}
}
}
`

View File

@ -89,9 +89,19 @@
<ds-heading tag="h3" soft style="text-align: center; margin-bottom: 10px;">
{{ $t('profile.network.title') }}
</ds-heading>
<follow-list :user="user" type="followedBy" />
<follow-list
:user="user"
type="followedBy"
@fetchAllConnections="fetchAllConnections"
:loading="$apollo.loading"
/>
<ds-space />
<follow-list :user="user" type="following" />
<follow-list
:user="user"
type="following"
@fetchAllConnections="fetchAllConnections"
:loading="$apollo.loading"
/>
<ds-space v-if="user.socialMedia && user.socialMedia.length" margin="large">
<base-card style="position: relative; height: auto;">
<ds-space margin="x-small">
@ -270,6 +280,8 @@ export default {
tabActive: 'post',
filter,
followedByCountStartValue: 0,
followedByCount: 7,
followingCount: 7,
}
},
computed: {
@ -299,13 +311,6 @@ export default {
return slug && `@${slug}`
},
},
watch: {
User(val) {
if (!val || !val.length) {
throw new Error('User not found!')
}
},
},
methods: {
removePostFromList(deletedPost) {
this.posts = this.posts.filter((post) => {
@ -427,6 +432,9 @@ export default {
this.user.followedByCurrentUser = followedByCurrentUser
this.user.followedBy = followedBy
},
fetchAllConnections(type) {
this[`${type}Count`] = Infinity
},
},
apollo: {
profilePagePosts: {
@ -451,7 +459,11 @@ export default {
return UserQuery(this.$i18n)
},
variables() {
return { id: this.$route.params.id }
return {
id: this.$route.params.id,
followedByCount: this.followedByCount || 7,
followingCount: this.followingCount || 7,
}
},
fetchPolicy: 'cache-and-network',
},