From 8d77731bbf1a7ac29c26c156678b65ba8a3c53df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Apr 2019 13:08:41 +0200 Subject: [PATCH 01/11] Remove obsolete number prefix on cypress tests Cypress tests don't need to be executed in order so we don't need the numbers anymore. cc @appinteractive --- .../{04.AboutMeAndLocation.feature => AboutMeAndLocation.feature} | 0 ....Internationalization.feature => Internationalization.feature} | 0 cypress/integration/{01.Login.feature => Login.feature} | 0 cypress/integration/{06.Search.feature => Search.feature} | 0 cypress/integration/{06.WritePost.feature => WritePost.feature} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename cypress/integration/{04.AboutMeAndLocation.feature => AboutMeAndLocation.feature} (100%) rename cypress/integration/{02.Internationalization.feature => Internationalization.feature} (100%) rename cypress/integration/{01.Login.feature => Login.feature} (100%) rename cypress/integration/{06.Search.feature => Search.feature} (100%) rename cypress/integration/{06.WritePost.feature => WritePost.feature} (100%) diff --git a/cypress/integration/04.AboutMeAndLocation.feature b/cypress/integration/AboutMeAndLocation.feature similarity index 100% rename from cypress/integration/04.AboutMeAndLocation.feature rename to cypress/integration/AboutMeAndLocation.feature diff --git a/cypress/integration/02.Internationalization.feature b/cypress/integration/Internationalization.feature similarity index 100% rename from cypress/integration/02.Internationalization.feature rename to cypress/integration/Internationalization.feature diff --git a/cypress/integration/01.Login.feature b/cypress/integration/Login.feature similarity index 100% rename from cypress/integration/01.Login.feature rename to cypress/integration/Login.feature diff --git a/cypress/integration/06.Search.feature b/cypress/integration/Search.feature similarity index 100% rename from cypress/integration/06.Search.feature rename to cypress/integration/Search.feature diff --git a/cypress/integration/06.WritePost.feature b/cypress/integration/WritePost.feature similarity index 100% rename from cypress/integration/06.WritePost.feature rename to cypress/integration/WritePost.feature From 8475884338f4eec1948d5f79874884ff6957878a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Apr 2019 13:46:35 +0200 Subject: [PATCH 02/11] Sketch :cucumber: for #345 --- cypress/integration/common/steps.js | 16 +++++++++++++++ .../notifications/mentions.feature | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 cypress/integration/notifications/mentions.feature diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 8944b7c25..b65bc96ce 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -293,3 +293,19 @@ Then('I can login successfully with password {string}', password => { }) cy.get('.iziToast-wrapper').should('contain', "You are logged in!") }) + +When('I log in with the following credentials:', table => { + const { email, password } = table.hashes()[0] + cy.login({ email, password }) +}) + +When('open the notification menu and click on the first item', () => { +}) + +Then('see {int} unread notifications in the top menu', count => { +}) + +Then('I get to the post page of {string}', path => { + path = path.replace('...', '') + cy.location('pathname').should('contain', `/post/${path}`) +}) diff --git a/cypress/integration/notifications/mentions.feature b/cypress/integration/notifications/mentions.feature new file mode 100644 index 000000000..7fcb34c77 --- /dev/null +++ b/cypress/integration/notifications/mentions.feature @@ -0,0 +1,20 @@ +Feature: Notifications for a mentioning + As a user + I want to be notified if sb. mentions me in a post or comment + In order join conversations about or related to me + + Background: + Given we have the following user accounts: + | name | slug | email | password | + | Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 | + And we have the following posts in our database: + | id | title | content | + | p1 | Hey Wolle | Hey @wolle-aus-hamburg, how do you do? | + + Scenario: + When I log in with the following credentials: + | email | password | + | wolle@example.org | 1234 | + And see 1 unread notifications in the top menu + And open the notification menu and click on the first item + Then I get to the post page of ".../hey-wolle" From 3ceb4373e31373ee935162cb785d9e647ffe8992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Apr 2019 14:26:08 +0200 Subject: [PATCH 03/11] Create a simple dropdown for notifications --- cypress/integration/common/steps.js | 1 + webapp/layouts/default.vue | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index b65bc96ce..6ccde03f4 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -303,6 +303,7 @@ When('open the notification menu and click on the first item', () => { }) Then('see {int} unread notifications in the top menu', count => { + cy.find('.notifications-menu').should('contain', count) }) Then('I get to the post page of {string}', path => { diff --git a/webapp/layouts/default.vue b/webapp/layouts/default.vue index 991662350..77aa32b2b 100644 --- a/webapp/layouts/default.vue +++ b/webapp/layouts/default.vue @@ -31,6 +31,27 @@ /> @@ -145,6 +143,7 @@ import LocaleSwitch from '~/components/LocaleSwitch' import Dropdown from '~/components/Dropdown' import SearchInput from '~/components/SearchInput.vue' import Modal from '~/components/Modal' +import Notification from '~/components/Notification' import seo from '~/components/mixins/seo' export default { @@ -153,7 +152,8 @@ export default { LocaleSwitch, SearchInput, Modal, - LocaleSwitch + LocaleSwitch, + Notification }, mixins: [seo], data() { diff --git a/webapp/store/auth.js b/webapp/store/auth.js index 688b8bed1..ad55f8b6c 100644 --- a/webapp/store/auth.js +++ b/webapp/store/auth.js @@ -89,7 +89,11 @@ export const actions = { createdAt post { author { + id + slug name + disabled + deleted } title contentExcerpt From 8701b220c1f1697471134a185766f337adcb5da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 9 Apr 2019 00:06:03 +0200 Subject: [PATCH 06/11] Implement first test case for NotificationList --- webapp/components/PostCard.vue | 9 +- .../{ => notifications}/Notification.vue | 5 +- .../notifications/NotificationList.spec.js | 139 ++++++++++++++++++ .../notifications/NotificationList.vue | 35 +++++ webapp/layouts/default.vue | 13 +- webapp/store/notifications.js | 5 + 6 files changed, 190 insertions(+), 16 deletions(-) rename webapp/components/{ => notifications}/Notification.vue (81%) create mode 100644 webapp/components/notifications/NotificationList.spec.js create mode 100644 webapp/components/notifications/NotificationList.vue create mode 100644 webapp/store/notifications.js diff --git a/webapp/components/PostCard.vue b/webapp/components/PostCard.vue index 87f56e7e3..83b800031 100644 --- a/webapp/components/PostCard.vue +++ b/webapp/components/PostCard.vue @@ -4,11 +4,12 @@ :image="post.image" :class="{'post-card': true, 'disabled-content': post.disabled}" > - {{ post.title }} + :to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }" + > + {{ post.title }} + diff --git a/webapp/components/Notification.vue b/webapp/components/notifications/Notification.vue similarity index 81% rename from webapp/components/Notification.vue rename to webapp/components/notifications/Notification.vue index 53b73035c..8f6db439e 100644 --- a/webapp/components/Notification.vue +++ b/webapp/components/notifications/Notification.vue @@ -1,8 +1,9 @@ @@ -10,7 +11,7 @@ import HcPostCard from '~/components/PostCard.vue' export default { - name: 'HcNotification', + name: 'Notification', components: { HcPostCard }, diff --git a/webapp/components/notifications/NotificationList.spec.js b/webapp/components/notifications/NotificationList.spec.js new file mode 100644 index 000000000..fc9f85da3 --- /dev/null +++ b/webapp/components/notifications/NotificationList.spec.js @@ -0,0 +1,139 @@ +import { + config, + shallowMount, + mount, + createLocalVue, + RouterLinkStub +} from '@vue/test-utils' +import NotificationList from './NotificationList.vue' +import Notification from './Notification.vue' +import Vue from 'vue' +import Vuex from 'vuex' + +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) +localVue.filter('truncate', string => string) + +config.stubs['no-ssr'] = '' +config.stubs['v-popover'] = '' + +describe('NotificationList.vue', () => { + let wrapper + let Wrapper + let propsData + let mocks + let stubs + let getters + let actions + let user + + beforeEach(() => { + mocks = { + $t: jest.fn() + } + stubs = { + NuxtLink: RouterLinkStub + } + actions = { + 'notifications/markAsRead': jest.fn() + } + getters = { + 'auth/user': () => { + return { + notifications: [ + { + id: 'notification-41', + read: false, + post: { + id: 'post-1', + title: 'some post title', + contentExcerpt: 'this is a post content', + author: { + id: 'john-1', + slug: 'john-doe', + name: 'John Doe' + } + } + }, + { + id: 'notification-42', + read: false, + post: { + id: 'post-2', + title: 'another post title', + contentExcerpt: 'this is yet another post content', + author: { + id: 'john-1', + slug: 'john-doe', + name: 'John Doe' + } + } + } + ] + } + } + } + }) + + describe('shallowMount', () => { + const Wrapper = () => { + const store = new Vuex.Store({ getters, actions }) + return shallowMount(NotificationList, { + store, + propsData, + mocks, + localVue + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders Notification.vue for each notification of the user', () => { + expect(wrapper.findAll(Notification)).toHaveLength(2) + }) + }) + + describe('mount', () => { + const Wrapper = () => { + const store = new Vuex.Store({ getters, actions }) + return mount(NotificationList, { + store, + propsData, + mocks, + stubs, + localVue + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + describe('click on a notification', () => { + beforeEach(() => { + wrapper + .findAll('a') + .at(1) + .trigger('click') + }) + + it('marks notification as read', () => { + expect(actions['notifications/markAsRead'].mock.calls[0][1]).toEqual(42) + }) + + describe('given mutation resolves', () => { + it.skip('updates currentUser.notifications', () => {}) + }) + + describe('given mutation rejects', () => { + it.skip('displays error warning', () => {}) + }) + }) + }) +}) diff --git a/webapp/components/notifications/NotificationList.vue b/webapp/components/notifications/NotificationList.vue new file mode 100644 index 000000000..7f4cc0e95 --- /dev/null +++ b/webapp/components/notifications/NotificationList.vue @@ -0,0 +1,35 @@ + + + diff --git a/webapp/layouts/default.vue b/webapp/layouts/default.vue index 2f30ad7db..44219a319 100644 --- a/webapp/layouts/default.vue +++ b/webapp/layouts/default.vue @@ -49,11 +49,7 @@ slot="popover" >
- +
@@ -143,7 +139,7 @@ import LocaleSwitch from '~/components/LocaleSwitch' import Dropdown from '~/components/Dropdown' import SearchInput from '~/components/SearchInput.vue' import Modal from '~/components/Modal' -import Notification from '~/components/Notification' +import NotificationList from '~/components/notifications/NotificationList' import seo from '~/components/mixins/seo' export default { @@ -153,7 +149,7 @@ export default { SearchInput, Modal, LocaleSwitch, - Notification + NotificationList }, mixins: [seo], data() { @@ -162,9 +158,6 @@ export default { } }, computed: { - notifications() { - return this.user.notifications - }, ...mapGetters({ user: 'auth/user', isLoggedIn: 'auth/isLoggedIn', diff --git a/webapp/store/notifications.js b/webapp/store/notifications.js new file mode 100644 index 000000000..c596f6b15 --- /dev/null +++ b/webapp/store/notifications.js @@ -0,0 +1,5 @@ +export const actions = { + async markAsRead(_, notificationId) { + console.log('notificationId', notificationId) + } +} From 8dd461d850353ebb6b493baad718474ade89e773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 9 Apr 2019 12:09:58 +0200 Subject: [PATCH 07/11] Implement NotificationList without store implement --- backend/src/seed/seed-db.js | 2 +- .../notifications/NotificationList.spec.js | 14 ++++---------- .../notifications/NotificationList.vue | 2 +- webapp/store/auth.js | 17 ----------------- webapp/store/notifications.js | 5 ----- 5 files changed, 6 insertions(+), 34 deletions(-) delete mode 100644 webapp/store/notifications.js diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js index 45b4d87a9..c8968a90f 100644 --- a/backend/src/seed/seed-db.js +++ b/backend/src/seed/seed-db.js @@ -102,7 +102,7 @@ import Factory from './factories' asTick.create('Post', { id: 'p9' }), asTrick.create('Post', { id: 'p10' }), asTrack.create('Post', { id: 'p11' }), - asAdmin.create('Post', { id: 'p12' }), + asAdmin.create('Post', { id: 'p12', content: `Hey @jenny-rostock, here is another notification for you! ${faker.lorem.paragraph()}` }), asModerator.create('Post', { id: 'p13' }), asUser.create('Post', { id: 'p14' }), asTick.create('Post', { id: 'p15' }) diff --git a/webapp/components/notifications/NotificationList.spec.js b/webapp/components/notifications/NotificationList.spec.js index fc9f85da3..3f120f397 100644 --- a/webapp/components/notifications/NotificationList.spec.js +++ b/webapp/components/notifications/NotificationList.spec.js @@ -118,21 +118,15 @@ describe('NotificationList.vue', () => { describe('click on a notification', () => { beforeEach(() => { wrapper - .findAll('a') + .findAll(Notification) .at(1) .trigger('click') }) it('marks notification as read', () => { - expect(actions['notifications/markAsRead'].mock.calls[0][1]).toEqual(42) - }) - - describe('given mutation resolves', () => { - it.skip('updates currentUser.notifications', () => {}) - }) - - describe('given mutation rejects', () => { - it.skip('displays error warning', () => {}) + expect(actions['notifications/markAsRead'].mock.calls[0][1]).toEqual( + 'notification-42' + ) }) }) }) diff --git a/webapp/components/notifications/NotificationList.vue b/webapp/components/notifications/NotificationList.vue index 7f4cc0e95..3988f257c 100644 --- a/webapp/components/notifications/NotificationList.vue +++ b/webapp/components/notifications/NotificationList.vue @@ -4,7 +4,7 @@ v-for="notification in notifications" :key="notification.id" :notification="notification" - @read="markAsRead(42)" + @read="markAsRead(notification.id)" /> diff --git a/webapp/store/auth.js b/webapp/store/auth.js index ad55f8b6c..4785ff0c0 100644 --- a/webapp/store/auth.js +++ b/webapp/store/auth.js @@ -83,23 +83,6 @@ export const actions = { role about locationName - notifications(read: false, orderBy: createdAt_desc) { - id - read - createdAt - post { - author { - id - slug - name - disabled - deleted - } - title - contentExcerpt - slug - } - } } }`) }) diff --git a/webapp/store/notifications.js b/webapp/store/notifications.js deleted file mode 100644 index c596f6b15..000000000 --- a/webapp/store/notifications.js +++ /dev/null @@ -1,5 +0,0 @@ -export const actions = { - async markAsRead(_, notificationId) { - console.log('notificationId', notificationId) - } -} From a0ef6b2dc39bf949e30b54e2be4d5b7952444cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 9 Apr 2019 16:38:58 +0200 Subject: [PATCH 08/11] Implement NotificationList with apollo --- .../notifications/NotificationList.spec.js | 94 +++++++++---------- .../notifications/NotificationList.vue | 54 ++++++++--- 2 files changed, 90 insertions(+), 58 deletions(-) diff --git a/webapp/components/notifications/NotificationList.spec.js b/webapp/components/notifications/NotificationList.spec.js index 3f120f397..1b4355c08 100644 --- a/webapp/components/notifications/NotificationList.spec.js +++ b/webapp/components/notifications/NotificationList.spec.js @@ -24,68 +24,70 @@ config.stubs['v-popover'] = '' describe('NotificationList.vue', () => { let wrapper let Wrapper - let propsData let mocks let stubs - let getters - let actions let user + let data + let store + let markAsRead beforeEach(() => { + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return {} + } + } + }) mocks = { $t: jest.fn() } stubs = { NuxtLink: RouterLinkStub } - actions = { - 'notifications/markAsRead': jest.fn() - } - getters = { - 'auth/user': () => { - return { - notifications: [ - { - id: 'notification-41', - read: false, - post: { - id: 'post-1', - title: 'some post title', - contentExcerpt: 'this is a post content', - author: { - id: 'john-1', - slug: 'john-doe', - name: 'John Doe' - } - } - }, - { - id: 'notification-42', - read: false, - post: { - id: 'post-2', - title: 'another post title', - contentExcerpt: 'this is yet another post content', - author: { - id: 'john-1', - slug: 'john-doe', - name: 'John Doe' - } + markAsRead = jest.fn() + data = () => { + return { + notifications: [ + { + id: 'notification-41', + read: false, + post: { + id: 'post-1', + title: 'some post title', + contentExcerpt: 'this is a post content', + author: { + id: 'john-1', + slug: 'john-doe', + name: 'John Doe' } } - ] - } + }, + { + id: 'notification-42', + read: false, + post: { + id: 'post-2', + title: 'another post title', + contentExcerpt: 'this is yet another post content', + author: { + id: 'john-1', + slug: 'john-doe', + name: 'John Doe' + } + } + } + ] } } }) describe('shallowMount', () => { const Wrapper = () => { - const store = new Vuex.Store({ getters, actions }) return shallowMount(NotificationList, { - store, - propsData, + data, mocks, + store, localVue }) } @@ -101,12 +103,11 @@ describe('NotificationList.vue', () => { describe('mount', () => { const Wrapper = () => { - const store = new Vuex.Store({ getters, actions }) return mount(NotificationList, { - store, - propsData, + data, mocks, stubs, + store, localVue }) } @@ -117,6 +118,7 @@ describe('NotificationList.vue', () => { describe('click on a notification', () => { beforeEach(() => { + wrapper.setMethods({ markAsRead }) wrapper .findAll(Notification) .at(1) @@ -124,9 +126,7 @@ describe('NotificationList.vue', () => { }) it('marks notification as read', () => { - expect(actions['notifications/markAsRead'].mock.calls[0][1]).toEqual( - 'notification-42' - ) + expect(markAsRead).toBeCalledWith('notification-42') }) }) }) diff --git a/webapp/components/notifications/NotificationList.vue b/webapp/components/notifications/NotificationList.vue index 3988f257c..ec76e8530 100644 --- a/webapp/components/notifications/NotificationList.vue +++ b/webapp/components/notifications/NotificationList.vue @@ -11,25 +11,57 @@ From 5b1348e6fbd398657aad7e200a3b29deb6c79133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 9 Apr 2019 19:17:47 +0200 Subject: [PATCH 09/11] Refactor: Notification as a root component --- .../notifications/NotificationList.spec.js | 74 +++++++-------- .../notifications/NotificationList.vue | 49 ++-------- .../notifications/NotificationMenu.vue | 92 +++++++++++++++++++ webapp/layouts/default.vue | 33 +------ 4 files changed, 138 insertions(+), 110 deletions(-) create mode 100644 webapp/components/notifications/NotificationMenu.vue diff --git a/webapp/components/notifications/NotificationList.spec.js b/webapp/components/notifications/NotificationList.spec.js index 1b4355c08..94da7869d 100644 --- a/webapp/components/notifications/NotificationList.spec.js +++ b/webapp/components/notifications/NotificationList.spec.js @@ -27,9 +27,8 @@ describe('NotificationList.vue', () => { let mocks let stubs let user - let data let store - let markAsRead + let propsData beforeEach(() => { store = new Vuex.Store({ @@ -45,47 +44,44 @@ describe('NotificationList.vue', () => { stubs = { NuxtLink: RouterLinkStub } - markAsRead = jest.fn() - data = () => { - return { - notifications: [ - { - id: 'notification-41', - read: false, - post: { - id: 'post-1', - title: 'some post title', - contentExcerpt: 'this is a post content', - author: { - id: 'john-1', - slug: 'john-doe', - name: 'John Doe' - } - } - }, - { - id: 'notification-42', - read: false, - post: { - id: 'post-2', - title: 'another post title', - contentExcerpt: 'this is yet another post content', - author: { - id: 'john-1', - slug: 'john-doe', - name: 'John Doe' - } + propsData = { + notifications: [ + { + id: 'notification-41', + read: false, + post: { + id: 'post-1', + title: 'some post title', + contentExcerpt: 'this is a post content', + author: { + id: 'john-1', + slug: 'john-doe', + name: 'John Doe' } } - ] - } + }, + { + id: 'notification-42', + read: false, + post: { + id: 'post-2', + title: 'another post title', + contentExcerpt: 'this is yet another post content', + author: { + id: 'john-1', + slug: 'john-doe', + name: 'John Doe' + } + } + } + ] } }) describe('shallowMount', () => { const Wrapper = () => { return shallowMount(NotificationList, { - data, + propsData, mocks, store, localVue @@ -104,7 +100,7 @@ describe('NotificationList.vue', () => { describe('mount', () => { const Wrapper = () => { return mount(NotificationList, { - data, + propsData, mocks, stubs, store, @@ -118,15 +114,15 @@ describe('NotificationList.vue', () => { describe('click on a notification', () => { beforeEach(() => { - wrapper.setMethods({ markAsRead }) wrapper .findAll(Notification) .at(1) .trigger('click') }) - it('marks notification as read', () => { - expect(markAsRead).toBeCalledWith('notification-42') + it("emits 'markAsRead' with the notificationId", () => { + expect(wrapper.emitted('markAsRead')).toBeTruthy() + expect(wrapper.emitted('markAsRead')[0]).toEqual(['notification-42']) }) }) }) diff --git a/webapp/components/notifications/NotificationList.vue b/webapp/components/notifications/NotificationList.vue index ec76e8530..2d6cd3828 100644 --- a/webapp/components/notifications/NotificationList.vue +++ b/webapp/components/notifications/NotificationList.vue @@ -11,56 +11,21 @@ + + diff --git a/webapp/layouts/default.vue b/webapp/layouts/default.vue index 44219a319..dd518e213 100644 --- a/webapp/layouts/default.vue +++ b/webapp/layouts/default.vue @@ -32,27 +32,7 @@