From a4f5fdc3242515999c674358230156009fcf373b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 2 May 2019 07:22:32 +0200 Subject: [PATCH 1/7] Make SocialMedia Links to target="_blank" --- webapp/pages/settings/my-social-media.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/pages/settings/my-social-media.vue b/webapp/pages/settings/my-social-media.vue index c031f54a4..f1714655f 100644 --- a/webapp/pages/settings/my-social-media.vue +++ b/webapp/pages/settings/my-social-media.vue @@ -10,7 +10,10 @@ v-for="link in socialMediaLinks" :key="link.url" > - + Social Media link Date: Thu, 2 May 2019 17:41:54 +0200 Subject: [PATCH 2/7] First Vue design of delete SocialMedia, custom mutation DeleteSocialMedia Backend Jest tests for DeleteSocialMedia New backend Jest tests for CreateSocialMedia --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/resolvers/socialMedia.js | 8 +++ backend/src/resolvers/socialMedia.spec.js | 58 ++++++++++++++++++- webapp/pages/settings/my-social-media.vue | 49 +++++++++++----- 4 files changed, 99 insertions(+), 17 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 3688aec16..85c584407 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -75,6 +75,7 @@ const permissions = shield({ DeleteBadge: isAdmin, AddUserBadges: isAdmin, CreateSocialMedia: isAuthenticated, + DeleteSocialMedia: isAuthenticated, // AddBadgeRewarded: isAdmin, // RemoveBadgeRewarded: isAdmin, reward: isAdmin, diff --git a/backend/src/resolvers/socialMedia.js b/backend/src/resolvers/socialMedia.js index 310375820..ef143a478 100644 --- a/backend/src/resolvers/socialMedia.js +++ b/backend/src/resolvers/socialMedia.js @@ -3,6 +3,9 @@ import { neo4jgraphql } from 'neo4j-graphql-js' export default { Mutation: { CreateSocialMedia: async (object, params, context, resolveInfo) => { + /** + * TODO?: Creates double Nodes! + */ const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session() await session.run( @@ -15,6 +18,11 @@ export default { ) session.close() + return socialMedia + }, + DeleteSocialMedia: async (object, params, context, resolveInfo) => { + const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false) + return socialMedia } } diff --git a/backend/src/resolvers/socialMedia.spec.js b/backend/src/resolvers/socialMedia.spec.js index b97316543..b09dae178 100644 --- a/backend/src/resolvers/socialMedia.spec.js +++ b/backend/src/resolvers/socialMedia.spec.js @@ -7,9 +7,18 @@ const factory = Factory() describe('CreateSocialMedia', () => { let client let headers - const mutation = ` + const mutationC = ` mutation($url: String!) { CreateSocialMedia(url: $url) { + id + url + } + } + ` + const mutationD = ` + mutation($id: ID!) { + DeleteSocialMedia(id: $id) { + id url } } @@ -30,20 +39,63 @@ describe('CreateSocialMedia', () => { await factory.cleanDatabase() }) + describe('unauthenticated', () => { + it('throws authorization error', async () => { + client = new GraphQLClient(host) + const variables = { url: 'http://nsosp.org' } + await expect( + client.request(mutationC, variables) + ).rejects.toThrow('Not Authorised') + }) + }) + describe('authenticated', () => { beforeEach(async () => { headers = await login({ email: 'test@example.org', password: '1234' }) client = new GraphQLClient(host, { headers }) }) + it('creates social media with correct URL', async () => { + const variables = { url: 'http://nsosp.org' } + await expect( + client.request(mutationC, variables) + ).resolves.toEqual(expect.objectContaining({ + CreateSocialMedia: { + id: expect.any(String), + url: 'http://nsosp.org' + } + })) + }) + + it('deletes social media', async () => { + const variablesC = { url: 'http://nsosp.org' } + const { CreateSocialMedia } = await client.request(mutationC, variablesC) + const { id } = CreateSocialMedia + + const variablesD = { id } + const expected = { + DeleteSocialMedia: { + id: id, + url: 'http://nsosp.org' + } + } + await expect( + client.request(mutationD, variablesD) + ).resolves.toEqual(expected) + }) + it('rejects empty string', async () => { const variables = { url: '' } - await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') + await expect( + client.request(mutationC, variables) + ).rejects.toThrow('Input is not a URL') }) it('validates URLs', async () => { const variables = { url: 'not-a-url' } - await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') + await expect( + client.request(mutationC, variables) + ).rejects.toThrow('Input is not a URL') }) }) }) diff --git a/webapp/pages/settings/my-social-media.vue b/webapp/pages/settings/my-social-media.vue index f1714655f..425f4c726 100644 --- a/webapp/pages/settings/my-social-media.vue +++ b/webapp/pages/settings/my-social-media.vue @@ -22,26 +22,38 @@ > {{ link.url }} +    |    + + + + -
- -
- - {{ $t('settings.social-media.submit') }} - +
+ +
+ + {{ $t('settings.social-media.submit') }} + +
+
@@ -104,7 +116,16 @@ export default { this.$toast.success(this.$t('settings.social-media.success')), (this.value = '') ) + }, + onDelete(link) { + console.log(link) } } } + + From 3294cffa88bb150018b09fc2ba262f0283a3e322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 2 May 2019 18:09:46 +0200 Subject: [PATCH 3/7] Renamed handleDeleteSocialMedia --- webapp/pages/settings/my-social-media.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/pages/settings/my-social-media.vue b/webapp/pages/settings/my-social-media.vue index 425f4c726..0eba9c3f0 100644 --- a/webapp/pages/settings/my-social-media.vue +++ b/webapp/pages/settings/my-social-media.vue @@ -28,7 +28,7 @@ class="layout-leave-active" /> @@ -117,7 +117,7 @@ export default { (this.value = '') ) }, - onDelete(link) { + handleDeleteSocialMedia(link) { console.log(link) } } From 2af9b853a14f2f168b164a89a44aa18cad6b89af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 4 May 2019 10:13:40 +0200 Subject: [PATCH 4/7] Add first try of Webapp Component Test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `ìd` to the SocialMedia data --- webapp/pages/settings/my-social-media.spec.js | 6 ++++++ webapp/pages/settings/my-social-media.vue | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/webapp/pages/settings/my-social-media.spec.js b/webapp/pages/settings/my-social-media.spec.js index 4f48a2835..b8c2f8182 100644 --- a/webapp/pages/settings/my-social-media.spec.js +++ b/webapp/pages/settings/my-social-media.spec.js @@ -71,6 +71,12 @@ describe('my-social-media.vue', () => { const socialMediaLink = wrapper.find('a').attributes().href expect(socialMediaLink).toBe(socialMediaUrl) }) + + it('displays a trash sympol after a social media', () => { + wrapper = Wrapper() + iconName = wrapper.find('.ds-icon').attributes().name + expect(iconName).toBe('trash') + }) }) describe('currentUser does not have a social media account linked', () => { diff --git a/webapp/pages/settings/my-social-media.vue b/webapp/pages/settings/my-social-media.vue index 0eba9c3f0..5b3914ff0 100644 --- a/webapp/pages/settings/my-social-media.vue +++ b/webapp/pages/settings/my-social-media.vue @@ -28,6 +28,7 @@ class="layout-leave-active" /> @@ -74,13 +75,13 @@ export default { socialMediaLinks() { const { socialMedia = [] } = this.currentUser return socialMedia.map(socialMedia => { - const { url } = socialMedia + const { id, url } = socialMedia const matches = url.match( /^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:\/\n?]+)/g ) const [domain] = matches || [] const favicon = domain ? `${domain}/favicon.ico` : null - return { url, favicon } + return { id, url, favicon } }) } }, From 41711c316acf1d531dc3ae349d7640e909b49f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 6 May 2019 17:31:02 +0200 Subject: [PATCH 5/7] Get delete SocialMedia to work, refactored Frontend Jest tests, written Cypress tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optimised tests and Vue for add Social Media a bit. Added localisation. Finished this commit together with @mattwr18 !!! Thank you so much dude! You did great stuff … --- backend/src/resolvers/socialMedia.spec.js | 8 ++-- cypress/integration/common/settings.js | 19 +++++++- .../user_profile/SocialMedia.feature | 9 ++++ webapp/locales/de.json | 4 +- webapp/locales/en.json | 4 +- webapp/pages/settings/my-social-media.spec.js | 4 +- webapp/pages/settings/my-social-media.vue | 44 ++++++++++++++++--- 7 files changed, 75 insertions(+), 17 deletions(-) diff --git a/backend/src/resolvers/socialMedia.spec.js b/backend/src/resolvers/socialMedia.spec.js index b09dae178..9d1d76726 100644 --- a/backend/src/resolvers/socialMedia.spec.js +++ b/backend/src/resolvers/socialMedia.spec.js @@ -68,11 +68,11 @@ describe('CreateSocialMedia', () => { }) it('deletes social media', async () => { - const variablesC = { url: 'http://nsosp.org' } - const { CreateSocialMedia } = await client.request(mutationC, variablesC) + const creationVariables = { url: 'http://nsosp.org' } + const { CreateSocialMedia } = await client.request(mutationC, creationVariables) const { id } = CreateSocialMedia - const variablesD = { id } + const deletionVariables = { id } const expected = { DeleteSocialMedia: { id: id, @@ -80,7 +80,7 @@ describe('CreateSocialMedia', () => { } } await expect( - client.request(mutationD, variablesD) + client.request(mutationD, deletionVariables) ).resolves.toEqual(expected) }) diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js index e1f3cc5a8..fa2962dfb 100644 --- a/cypress/integration/common/settings.js +++ b/cypress/integration/common/settings.js @@ -77,7 +77,7 @@ Then('I should be on the {string} page', page => { .should('contain', 'Social media') }) -Then('I add a social media link', () => { +When('I add a social media link', () => { cy.get("input[name='social-media']") .type('https://freeradical.zone/peter-pan') .get('button') @@ -87,7 +87,7 @@ Then('I add a social media link', () => { Then('it gets saved successfully', () => { cy.get('.iziToast-message') - .should('contain', 'Updated user') + .should('contain', 'Added social media') }) Then('the new social media link shows up on the page', () => { @@ -110,3 +110,18 @@ Then('they should be able to see my social media links', () => { .get('a[href="https://freeradical.zone/peter-pan"]') .should('have.length', 1) }) + +When('I delete a social media link', () => { + cy.get("a[name='delete']") + .click() +}) + +// Then('Shows delete modal', () => { +// cy.get("a[name='delete']") +// .click() +// }) + +Then('it gets deleted successfully', () => { + cy.get('.iziToast-message') + .should('contain', 'Deleted social media') +}) diff --git a/cypress/integration/user_profile/SocialMedia.feature b/cypress/integration/user_profile/SocialMedia.feature index 988923c17..4466b9537 100644 --- a/cypress/integration/user_profile/SocialMedia.feature +++ b/cypress/integration/user_profile/SocialMedia.feature @@ -19,3 +19,12 @@ Feature: List Social Media Accounts Given I have added a social media link When people visit my profile page Then they should be able to see my social media links + + Scenario: Deleting Social Media + Given I am on the "settings" page + And I click on the "Social media" link + Then I should be on the "/settings/my-social-media" page + Given I have added a social media link + When I delete a social media link + Then it gets deleted successfully + # And the new social media link shows up on the page diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 7e69b5bbb..0d079cc15 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -63,8 +63,10 @@ }, "social-media": { "name": "Soziale Medien", + "placeholder": "Füge eine Social-Media URL hinzu", "submit": "Link hinzufügen", - "success": "Profil aktualisiert" + "successAdd": "Social-Media hinzugefügt. Profil aktualisiert!", + "successDelete": "Social-Media gelöscht. Profil aktualisiert!" } }, "admin": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 02bea7bae..65074e667 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -63,8 +63,10 @@ }, "social-media": { "name": "Social media", + "placeholder": "Add social media url", "submit": "Add link", - "success": "Updated user profile" + "successAdd": "Added social media. Updated user profile!", + "successDelete": "Deleted social media. Updated user profile!" } }, "admin": { diff --git a/webapp/pages/settings/my-social-media.spec.js b/webapp/pages/settings/my-social-media.spec.js index b8c2f8182..673c1cd70 100644 --- a/webapp/pages/settings/my-social-media.spec.js +++ b/webapp/pages/settings/my-social-media.spec.js @@ -74,8 +74,8 @@ describe('my-social-media.vue', () => { it('displays a trash sympol after a social media', () => { wrapper = Wrapper() - iconName = wrapper.find('.ds-icon').attributes().name - expect(iconName).toBe('trash') + const deleteSelector = wrapper.find({ name: 'delete' }) + expect(deleteSelector).toEqual({ selector: 'Component' }) }) }) diff --git a/webapp/pages/settings/my-social-media.vue b/webapp/pages/settings/my-social-media.vue index 5b3914ff0..d0d805730 100644 --- a/webapp/pages/settings/my-social-media.vue +++ b/webapp/pages/settings/my-social-media.vue @@ -8,7 +8,7 @@ {{ link.url }} -    |    +    |    @@ -95,6 +95,7 @@ export default { mutation: gql` mutation($url: String!) { CreateSocialMedia(url: $url) { + id url } } @@ -113,13 +114,42 @@ export default { }) } }) - .then( - this.$toast.success(this.$t('settings.social-media.success')), + .then(() => { + this.$toast.success(this.$t('settings.social-media.successAdd')), (this.value = '') - ) + }) + .catch(error => { + this.$toast.error(error.message) + }) }, handleDeleteSocialMedia(link) { - console.log(link) + this.$apollo + .mutate({ + mutation: gql` + mutation($id: ID!) { + DeleteSocialMedia(id: $id) { + id + url + } + } + `, + variables: { + id: link.id + }, + update: (store, { data }) => { + const socialMedia = this.currentUser.socialMedia.filter(element => element.id !== link.id ) + this.setCurrentUser({ + ...this.currentUser, + socialMedia + }) + } + }) + .then(() => { + this.$toast.success(this.$t('settings.social-media.successDelete')) + }) + .catch(error => { + this.$toast.error(error.message) + }) } } } From ff303fccd3e644f6ddead9558a32bf7fe031f3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 8 May 2019 14:14:34 +0200 Subject: [PATCH 6/7] Fixed linting errors --- webapp/pages/settings/my-social-media.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/webapp/pages/settings/my-social-media.vue b/webapp/pages/settings/my-social-media.vue index d0d805730..462e9af96 100644 --- a/webapp/pages/settings/my-social-media.vue +++ b/webapp/pages/settings/my-social-media.vue @@ -31,7 +31,7 @@ name="delete" @click="handleDeleteSocialMedia(link)" > - + @@ -116,7 +116,7 @@ export default { }) .then(() => { this.$toast.success(this.$t('settings.social-media.successAdd')), - (this.value = '') + (this.value = '') }) .catch(error => { this.$toast.error(error.message) @@ -137,7 +137,9 @@ export default { id: link.id }, update: (store, { data }) => { - const socialMedia = this.currentUser.socialMedia.filter(element => element.id !== link.id ) + const socialMedia = this.currentUser.socialMedia.filter( + element => element.id !== link.id + ) this.setCurrentUser({ ...this.currentUser, socialMedia From c4a4d3d1f02ec42a629b5a2558e68f4ce2961fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 8 May 2019 16:04:09 +0200 Subject: [PATCH 7/7] Wrote an additional frontend unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skipped writing of a modal dialog before deletion, because many code parts are on the way in the delete post PR. So makes no sense to write them twice. I make a new issue … --- cypress/integration/common/settings.js | 5 ---- .../user_profile/SocialMedia.feature | 1 - webapp/pages/settings/my-social-media.spec.js | 30 ++++++++++++++++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js index fa2962dfb..b6621ec87 100644 --- a/cypress/integration/common/settings.js +++ b/cypress/integration/common/settings.js @@ -116,11 +116,6 @@ When('I delete a social media link', () => { .click() }) -// Then('Shows delete modal', () => { -// cy.get("a[name='delete']") -// .click() -// }) - Then('it gets deleted successfully', () => { cy.get('.iziToast-message') .should('contain', 'Deleted social media') diff --git a/cypress/integration/user_profile/SocialMedia.feature b/cypress/integration/user_profile/SocialMedia.feature index 4466b9537..d21167c6b 100644 --- a/cypress/integration/user_profile/SocialMedia.feature +++ b/cypress/integration/user_profile/SocialMedia.feature @@ -27,4 +27,3 @@ Feature: List Social Media Accounts Given I have added a social media link When I delete a social media link Then it gets deleted successfully - # And the new social media link shows up on the page diff --git a/webapp/pages/settings/my-social-media.spec.js b/webapp/pages/settings/my-social-media.spec.js index 673c1cd70..559ba87d2 100644 --- a/webapp/pages/settings/my-social-media.spec.js +++ b/webapp/pages/settings/my-social-media.spec.js @@ -72,10 +72,38 @@ describe('my-social-media.vue', () => { expect(socialMediaLink).toBe(socialMediaUrl) }) - it('displays a trash sympol after a social media', () => { + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + mutate: jest + .fn() + .mockRejectedValue({ message: 'Ouch!' }) + .mockResolvedValueOnce({ + data: { DeleteSocialMeda: { id: 's1', url: socialMediaUrl } } + }) + }, + $toast: { + error: jest.fn(), + success: jest.fn() + } + } + getters = { + 'auth/user': () => { + return { + socialMedia: [{ id: 's1', url: socialMediaUrl }] + } + } + } + }) + + it('displays a trash sympol after a social media and allows the user to delete it', () => { wrapper = Wrapper() const deleteSelector = wrapper.find({ name: 'delete' }) expect(deleteSelector).toEqual({ selector: 'Component' }) + const icon = wrapper.find({ name: 'trash' }) + icon.trigger('click') + expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) }) })