From f62efe39fadb302c704d7fbdc0a150efad507ac6 Mon Sep 17 00:00:00 2001 From: Dries Cruyskens Date: Fri, 12 Jun 2020 18:28:40 +0200 Subject: [PATCH] Made profile header component that supports uploading and displaying a header image and added it to the profile page. Added locales in all languages for a succesful or failed upload toast. Included test for the new userProfileHeader component. Updated the gql model to include the profileHeader. --- .../UserProfileHeader/UserProfileHeader.vue | 224 ++++++++++++++++++ .../userProfileHeader.spec.js | 217 +++++++++++++++++ webapp/graphql/User.js | 8 + webapp/locales/de.json | 3 + webapp/locales/en.json | 3 + webapp/locales/es.json | 3 + webapp/locales/fr.json | 3 + webapp/locales/it.json | 3 + webapp/locales/nl.json | 5 + webapp/locales/pl.json | 3 + webapp/locales/pt.json | 3 + webapp/locales/ru.json | 3 + webapp/pages/profile/_id/_slug.vue | 13 +- 13 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 webapp/components/_new/generic/UserProfileHeader/UserProfileHeader.vue create mode 100644 webapp/components/_new/generic/UserProfileHeader/userProfileHeader.spec.js diff --git a/webapp/components/_new/generic/UserProfileHeader/UserProfileHeader.vue b/webapp/components/_new/generic/UserProfileHeader/UserProfileHeader.vue new file mode 100644 index 000000000..e6f8be41f --- /dev/null +++ b/webapp/components/_new/generic/UserProfileHeader/UserProfileHeader.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/webapp/components/_new/generic/UserProfileHeader/userProfileHeader.spec.js b/webapp/components/_new/generic/UserProfileHeader/userProfileHeader.spec.js new file mode 100644 index 000000000..5b4c96e41 --- /dev/null +++ b/webapp/components/_new/generic/UserProfileHeader/userProfileHeader.spec.js @@ -0,0 +1,217 @@ +import { mount, shallowMount } from '@vue/test-utils' +import UserProfileHeader from './UserProfileHeader.vue' +import vueDropzone from 'nuxt-dropzone' +import Vue from 'vue' + +const localVue = global.localVue + +describe('UserProfileHeader.vue', () => { + let propsData, wrapper, mocks + + beforeEach(() => { + propsData = {} + wrapper = Wrapper() + }) + + const Wrapper = () => { + return mount(UserProfileHeader, { propsData, localVue }) + } + + it('renders no image', () => { + expect(wrapper.contains('img')).toBe(false) + }) + + it('renders no dropzone', () => { + expect(wrapper.contains(vueDropzone)).toBe(false) + }) + + describe('given a user', () => { + describe('with no header image', () => { + beforeEach(() => { + propsData = { + user: { + name: 'Matt Rider', + }, + } + wrapper = Wrapper() + }) + + it('renders no img tag', () => { + expect(wrapper.contains('img')).toBe(false) + }) + }) + + describe('with a header image', () => { + beforeEach(() => { + propsData = { + user: { + name: 'Matt Rider', + profileHeader: { + url: 'https://source.unsplash.com/640x480', + }, + }, + } + wrapper = Wrapper() + }) + + it('renders an image', () => { + expect(wrapper.contains('img')).toBe(true) + }) + }) + + describe('with a relative avatar url', () => { + beforeEach(() => { + propsData = { + user: { + name: 'Not Anonymous', + profileHeader: { + url: '/profileHeader.jpg', + }, + }, + } + wrapper = Wrapper() + }) + + it('adds a prefix to load the image from the uploads service', () => { + expect(wrapper.find('img').attributes('src')).toBe('/api/profileHeader.jpg') + }) + }) + + describe('with an absolute avatar url', () => { + beforeEach(() => { + propsData = { + user: { + name: 'Not Anonymous', + profileHeader: { + url: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg', + }, + }, + } + wrapper = Wrapper() + }) + + it('keeps the avatar URL as is', () => { + // e.g. our seeds have absolute image URLs + expect(wrapper.find('img').attributes('src')).toBe( + 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg', + ) + }) + }) + + describe('on his own userpage', () => { + beforeEach(() => { + propsData = { + editable: true, + user: { + name: 'Not Anonymous', + profileHeader: { + url: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg', + }, + }, + } + wrapper = Wrapper() + }) + + it('a dropzone is present', () => { + expect(wrapper.contains(vueDropzone)).toBe(true) + }) + + describe('uploading and image', () => { + beforeAll(() => { + propsData = { + user: { + profileHeader: { url: '/api/profileHeader.jpg' }, + }, + } + mocks = { + $apollo: { + mutate: jest + .fn() + .mockResolvedValueOnce({ + data: { + UpdateUser: { + id: 'upload1', + profileHeader: { url: '/upload/profileHeader.jpg' }, + }, + }, + }) + .mockRejectedValue({ + message: 'File upload unsuccessful!', + }), + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $t: jest.fn(), + } + }) + + beforeEach(() => { + jest.useFakeTimers() + wrapper = shallowMount(UserProfileHeader, { localVue, propsData, mocks }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('sends a UpdateUser mutation when vddrop is called', () => { + wrapper.vm.vddrop([{ filename: 'profileHeader.jpg' }]) + expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) + }) + + describe('error handling', () => { + const message = 'File upload failed' + const fileError = { status: 'error' } + + it('defaults to error false', () => { + expect(wrapper.vm.error).toEqual(false) + }) + + it('shows an error toaster when verror is called', () => { + wrapper.vm.verror(fileError, message) + expect(mocks.$toast.error).toHaveBeenCalledWith(fileError.status, message) + }) + + it('changes error status from false to true to false', async () => { + wrapper.vm.verror(fileError, message) + await Vue.nextTick() + expect(wrapper.vm.error).toEqual(true) + jest.runAllTimers() + expect(wrapper.vm.error).toEqual(false) + }) + + it('shows an error toaster when the apollo mutation rejects', async () => { + // calls vddrop twice because of how mockResolvedValueOnce works in jest + // the first time the mock function is called it will resolve, calling it a + // second time will cause it to fail(with this implementation) + // https://jestjs.io/docs/en/mock-function-api.html#mockfnmockresolvedvalueoncevalue + await wrapper.vm.vddrop([{ filename: 'profileHeader.jpg' }]) + await wrapper.vm.vddrop([{ filename: 'profileHeader.jpg' }]) + expect(mocks.$toast.error).toHaveBeenCalledTimes(1) + }) + }) + }) + }) + + describe("on a different user's userpage", () => { + beforeEach(() => { + propsData = { + editable: false, + user: { + name: 'Not Anonymous', + profileHeader: { + url: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg', + }, + }, + } + wrapper = Wrapper() + }) + + it('no dropzone is present', () => { + expect(wrapper.contains(vueDropzone)).toBe(false) + }) + }) + }) +}) diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index daf586284..ffd7ed705 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -20,6 +20,9 @@ export default (i18n) => { ...userCounts ...locationAndBadges about + profileHeader { + url + } locationName createdAt followedByCurrentUser @@ -226,6 +229,7 @@ export const updateUserMutation = () => { $showShoutsPublicly: Boolean $termsAndConditionsAgreedVersion: String $avatar: ImageInput + $profileHeader: ImageInput ) { UpdateUser( id: $id @@ -237,6 +241,7 @@ export const updateUserMutation = () => { showShoutsPublicly: $showShoutsPublicly termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion avatar: $avatar + profileHeader: $profileHeader ) { id slug @@ -250,6 +255,9 @@ export const updateUserMutation = () => { avatar { url } + profileHeader { + url + } } } ` diff --git a/webapp/locales/de.json b/webapp/locales/de.json index d53e32a2b..59e3cbcb3 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -830,6 +830,9 @@ "user": { "avatar": { "submitted": "Erfolgreich hochgeladen!" + }, + "profileHeader": { + "submitted": "Erfolgreich hochgeladen!" } } } diff --git a/webapp/locales/en.json b/webapp/locales/en.json index e91f5c37a..98c1f41e4 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -830,6 +830,9 @@ "user": { "avatar": { "submitted": "Upload successful!" + }, + "profileHeader": { + "submitted": "Upload successful!" } } } diff --git a/webapp/locales/es.json b/webapp/locales/es.json index 0925687fc..d3de2228c 100644 --- a/webapp/locales/es.json +++ b/webapp/locales/es.json @@ -827,6 +827,9 @@ "user": { "avatar": { "submitted": "Carga con éxito" + }, + "profileHeader": { + "submitted": "Carga con éxito!" } } } diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json index e0bcf14e1..c3e2b7c2e 100644 --- a/webapp/locales/fr.json +++ b/webapp/locales/fr.json @@ -791,6 +791,9 @@ "user": { "avatar": { "submitted": "Téléchargement réussi" + }, + "profileHeader": { + "submitted": "Téléchargement réussi" } } } diff --git a/webapp/locales/it.json b/webapp/locales/it.json index b53b863cc..5f535b8c1 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -734,6 +734,9 @@ "user": { "avatar": { "submitted": "" + }, + "profileHeader": { + "submitted": "Caricamento eseguito correttamente!" } } } diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json index 64643133c..d1151bb3e 100644 --- a/webapp/locales/nl.json +++ b/webapp/locales/nl.json @@ -172,5 +172,10 @@ "taxident": "Identificatienummer voor de belasting over de toegevoegde waarde overeenkomstig § 27 a Wet op de belasting over de toegevoegde waarde (Duitsland).", "termsAc": "Gebruiksvoorwaarden", "tribunal": "registerrechtbank" + }, + "user": { + "profileHeader": { + "submitted": "Uploaden geslaagd!" + } } } diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index 0c7dd2f59..4015c55de 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -363,6 +363,9 @@ "user": { "avatar": { "submitted": "Przesłano pomyślnie" + }, + "profileHeader": { + "submitted": "Przesłano pomyślnie" } } } diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 1a3efeb44..956b2bf1a 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -722,6 +722,9 @@ "user": { "avatar": { "submitted": "Carregado com sucesso!" + }, + "profileHeader": { + "submitted": "Carregado com sucesso!" } } } diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json index 59928a2c5..f8097f48a 100644 --- a/webapp/locales/ru.json +++ b/webapp/locales/ru.json @@ -823,6 +823,9 @@ "user": { "avatar": { "submitted": "Успешная загрузка!" + }, + "profileHeader": { + "submitted": "Успешная загрузка!" } } } diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index 1a43da14b..c292f7315 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -1,7 +1,11 @@