From cb5280e6e8752be1d6cccb2dc1c1eedd2d76c1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Apr 2019 14:35:41 +0200 Subject: [PATCH 01/91] Sketch test to create a notificaion for a mention --- .../src/middleware/notificationMiddleware.js | 0 .../middleware/notificationMiddleware.spec.js | 85 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 backend/src/middleware/notificationMiddleware.js create mode 100644 backend/src/middleware/notificationMiddleware.spec.js diff --git a/backend/src/middleware/notificationMiddleware.js b/backend/src/middleware/notificationMiddleware.js new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/middleware/notificationMiddleware.spec.js b/backend/src/middleware/notificationMiddleware.spec.js new file mode 100644 index 000000000..ccb38fcbf --- /dev/null +++ b/backend/src/middleware/notificationMiddleware.spec.js @@ -0,0 +1,85 @@ +import Factory from '../seed/factories' +import { GraphQLClient } from 'graphql-request' +import { host, login } from '../jest/helpers' + +const factory = Factory() +let client + +beforeEach(async () => { + await factory.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + email: 'test@example.org', + password: '1234' + }) +}) + +afterEach(async () => { + await factory.cleanDatabase() +}) + +describe('currentUser { notifications }', () => { + const query = `query($read: Boolean) { + currentUser { + notifications(read: $read, orderBy: createdAt_desc) { + id + post { + id + } + } + } + }` + + describe('authenticated', () => { + let headers + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + describe('given another user', () => { + let authorClient + let authorParams + let authorHeaders + + beforeEach(async () => { + authorParams = { + email: 'author@example.org', + password: '1234', + id: 'author' + } + await factory.create('User', authorParams) + authorHeaders = await login(authorParams) + }) + + describe('who mentions me in a post', () => { + beforeEach(async () => { + const content = 'Hey @al-capone how do you do?' + const title = 'Mentioning Al Capone' + const createPostMutation = ` + mutation($title: String!, $content: String!) { + CreatePost(title: $title, content: $content) { + title + content + } + } + ` + authorClient = new GraphQLClient(host, authorHeaders) + await authorClient.request(createPostMutation, { title, content }) + }) + + it('sends you a notification', async () => { + const expected = { + currentUser: { + notifications: [ + { read: false, post: { content: 'Hey @al-capone how do you do?' } } + ] + } + } + await expect(client.request(query, { read: false })).resolves.toEqual(expected) + }) + }) + }) + }) +}) From cf1f655451277d98da70957d829def3b3022c97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sat, 6 Apr 2019 00:33:10 +0200 Subject: [PATCH 02/91] Create notifications on CreatePost --- backend/src/middleware/index.js | 2 ++ .../src/middleware/notificationMiddleware.js | 0 .../src/middleware/notificationsMiddleware.js | 31 +++++++++++++++++++ ...pec.js => notificationsMiddleware.spec.js} | 4 ++- 4 files changed, 36 insertions(+), 1 deletion(-) delete mode 100644 backend/src/middleware/notificationMiddleware.js create mode 100644 backend/src/middleware/notificationsMiddleware.js rename backend/src/middleware/{notificationMiddleware.spec.js => notificationsMiddleware.spec.js} (94%) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 8f86a88e6..8d893a78b 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -10,6 +10,7 @@ import permissionsMiddleware from './permissionsMiddleware' import userMiddleware from './userMiddleware' import includedFieldsMiddleware from './includedFieldsMiddleware' import orderByMiddleware from './orderByMiddleware' +import notificationsMiddleware from './notificationsMiddleware' export default schema => { let middleware = [ @@ -19,6 +20,7 @@ export default schema => { excerptMiddleware, xssMiddleware, fixImageUrlsMiddleware, + notificationsMiddleware, softDeleteMiddleware, userMiddleware, includedFieldsMiddleware, diff --git a/backend/src/middleware/notificationMiddleware.js b/backend/src/middleware/notificationMiddleware.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/middleware/notificationsMiddleware.js b/backend/src/middleware/notificationsMiddleware.js new file mode 100644 index 000000000..1150ab0d9 --- /dev/null +++ b/backend/src/middleware/notificationsMiddleware.js @@ -0,0 +1,31 @@ +const MENTION_REGEX = /@(\S+)/g + +const notify = async (resolve, root, args, context, resolveInfo) => { + const post = await resolve(root, args, context, resolveInfo) + + const session = context.driver.session() + const { content, id: postId } = post + const slugs = [] + const createdAt = (new Date()).toISOString() + let match + while ((match = MENTION_REGEX.exec(content)) != null) { + slugs.push(match[1]) + } + const cypher = ` + match(u:User) where u.slug in $slugs + match(p:Post) where p.id = $postId + create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt}) + merge (n)-[:NOTIFIED]->(u) + merge (p)-[:NOTIFIED]->(n) + ` + await session.run(cypher, { slugs, createdAt, postId }) + session.close() + + return post +} + +export default { + Mutation: { + CreatePost: notify + } +} diff --git a/backend/src/middleware/notificationMiddleware.spec.js b/backend/src/middleware/notificationsMiddleware.spec.js similarity index 94% rename from backend/src/middleware/notificationMiddleware.spec.js rename to backend/src/middleware/notificationsMiddleware.spec.js index ccb38fcbf..9fed4a59a 100644 --- a/backend/src/middleware/notificationMiddleware.spec.js +++ b/backend/src/middleware/notificationsMiddleware.spec.js @@ -24,8 +24,10 @@ describe('currentUser { notifications }', () => { currentUser { notifications(read: $read, orderBy: createdAt_desc) { id + read post { id + content } } } @@ -65,7 +67,7 @@ describe('currentUser { notifications }', () => { } } ` - authorClient = new GraphQLClient(host, authorHeaders) + authorClient = new GraphQLClient(host, { headers: authorHeaders }) await authorClient.request(createPostMutation, { title, content }) }) From 8e6fb3d9e2c7b818b3dde266b5a196915798cf13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Apr 2019 10:19:57 +0200 Subject: [PATCH 03/91] Fix test --- backend/src/middleware/notificationsMiddleware.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/middleware/notificationsMiddleware.spec.js b/backend/src/middleware/notificationsMiddleware.spec.js index 9fed4a59a..e6fc78c52 100644 --- a/backend/src/middleware/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notificationsMiddleware.spec.js @@ -23,10 +23,8 @@ describe('currentUser { notifications }', () => { const query = `query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { - id read post { - id content } } From f372cdcbdba94820407f69d1cfa727d2093e5b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Apr 2019 12:01:09 +0200 Subject: [PATCH 04/91] Avoid to send out notifications for email adresses --- backend/src/middleware/notifications/mentions.js | 10 ++++++++++ .../src/middleware/notifications/mentions.spec.js | 15 +++++++++++++++ backend/src/middleware/notificationsMiddleware.js | 8 ++------ 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 backend/src/middleware/notifications/mentions.js create mode 100644 backend/src/middleware/notifications/mentions.spec.js diff --git a/backend/src/middleware/notifications/mentions.js b/backend/src/middleware/notifications/mentions.js new file mode 100644 index 000000000..fb4a049f2 --- /dev/null +++ b/backend/src/middleware/notifications/mentions.js @@ -0,0 +1,10 @@ +const MENTION_REGEX = /\s@(\S+)/g + +export function extractSlugs(content) { + let slugs = [] + let match + while ((match = MENTION_REGEX.exec(content)) != null) { + slugs.push(match[1]) + } + return slugs +} diff --git a/backend/src/middleware/notifications/mentions.spec.js b/backend/src/middleware/notifications/mentions.spec.js new file mode 100644 index 000000000..8fe9221b3 --- /dev/null +++ b/backend/src/middleware/notifications/mentions.spec.js @@ -0,0 +1,15 @@ +import { extractSlugs } from './mentions' + +describe('extract', () => { + describe('finds mentions in the form of', () => { + it('@user', () => { + const content = 'Hello @user' + expect(extractSlugs(content)).toEqual(['user']) + }) + }) + + it('ignores email addresses', () => { + const content = 'Hello somebody@example.org' + expect(extractSlugs(content)).toEqual([]) + }) +}) diff --git a/backend/src/middleware/notificationsMiddleware.js b/backend/src/middleware/notificationsMiddleware.js index 1150ab0d9..30205278b 100644 --- a/backend/src/middleware/notificationsMiddleware.js +++ b/backend/src/middleware/notificationsMiddleware.js @@ -1,16 +1,12 @@ -const MENTION_REGEX = /@(\S+)/g +import { extractSlugs } from './notifications/mentions' const notify = async (resolve, root, args, context, resolveInfo) => { const post = await resolve(root, args, context, resolveInfo) const session = context.driver.session() const { content, id: postId } = post - const slugs = [] + const slugs = extractSlugs(content) const createdAt = (new Date()).toISOString() - let match - while ((match = MENTION_REGEX.exec(content)) != null) { - slugs.push(match[1]) - } const cypher = ` match(u:User) where u.slug in $slugs match(p:Post) where p.id = $postId From adde8a596557824a06274d6721fc53502609259b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Apr 2019 12:08:49 +0200 Subject: [PATCH 05/91] Remove dots from matched @mention regex --- backend/src/middleware/notifications/mentions.js | 2 +- .../src/middleware/notifications/mentions.spec.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/src/middleware/notifications/mentions.js b/backend/src/middleware/notifications/mentions.js index fb4a049f2..7071c9313 100644 --- a/backend/src/middleware/notifications/mentions.js +++ b/backend/src/middleware/notifications/mentions.js @@ -1,4 +1,4 @@ -const MENTION_REGEX = /\s@(\S+)/g +const MENTION_REGEX = /\s@([\w_-]+)/g export function extractSlugs(content) { let slugs = [] diff --git a/backend/src/middleware/notifications/mentions.spec.js b/backend/src/middleware/notifications/mentions.spec.js index 8fe9221b3..0c70aae1c 100644 --- a/backend/src/middleware/notifications/mentions.spec.js +++ b/backend/src/middleware/notifications/mentions.spec.js @@ -6,6 +6,21 @@ describe('extract', () => { const content = 'Hello @user' expect(extractSlugs(content)).toEqual(['user']) }) + + it('@user-with-dash', () => { + const content = 'Hello @user-with-dash' + expect(extractSlugs(content)).toEqual(['user-with-dash']) + }) + + it('@user.', () => { + const content = 'Hello @user.' + expect(extractSlugs(content)).toEqual(['user']) + }) + + it('@user-With-Capital-LETTERS', () => { + const content = 'Hello @user-With-Capital-LETTERS' + expect(extractSlugs(content)).toEqual(['user-With-Capital-LETTERS']) + }) }) it('ignores email addresses', () => { From b355a244ab8fce0943293cea6d79cd6e4285d255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 9 Apr 2019 20:51:17 +0200 Subject: [PATCH 06/91] Fix lint --- backend/src/middleware/notifications/mentions.js | 2 +- backend/src/middleware/notifications/mentions.spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/middleware/notifications/mentions.js b/backend/src/middleware/notifications/mentions.js index 7071c9313..137c23f1c 100644 --- a/backend/src/middleware/notifications/mentions.js +++ b/backend/src/middleware/notifications/mentions.js @@ -1,6 +1,6 @@ const MENTION_REGEX = /\s@([\w_-]+)/g -export function extractSlugs(content) { +export function extractSlugs (content) { let slugs = [] let match while ((match = MENTION_REGEX.exec(content)) != null) { diff --git a/backend/src/middleware/notifications/mentions.spec.js b/backend/src/middleware/notifications/mentions.spec.js index 0c70aae1c..f12df7f07 100644 --- a/backend/src/middleware/notifications/mentions.spec.js +++ b/backend/src/middleware/notifications/mentions.spec.js @@ -24,7 +24,7 @@ describe('extract', () => { }) it('ignores email addresses', () => { - const content = 'Hello somebody@example.org' - expect(extractSlugs(content)).toEqual([]) + const content = 'Hello somebody@example.org' + expect(extractSlugs(content)).toEqual([]) }) }) From f4fc2d6c3074c3433a9d6492ab7b0f173097c678 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 07/91] 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 35e2640f3733da9617947638775fa399d4e47520 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 08/91] 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 d0812dae092a30b74a34aa98e05bee6988e5b938 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 09/91] 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 13fe228b5893a1948ab210fb69e4e33a02248023 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 12/91] Implement first test case for NotificationList --- webapp/components/PostCard.vue | 7 +- .../{ => 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, 188 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 767835f74..8d9fac608 100644 --- a/webapp/components/PostCard.vue +++ b/webapp/components/PostCard.vue @@ -4,13 +4,12 @@ :image="post.image" :class="{'post-card': true, 'disabled-content': post.disabled}" > - {{ 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 512835f202cbe8b926ce2a4c2a05c620ba8ff4b8 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 13/91] Implement NotificationList without store implement --- backend/src/seed/seed-db.js | 2 +- .../notifications/NotificationList.spec.js | 14 ++++---------- .../notifications/NotificationList.vue | 2 +- webapp/store/notifications.js | 17 +++++++++++++++-- 4 files changed, 21 insertions(+), 14 deletions(-) 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/notifications.js b/webapp/store/notifications.js index c596f6b15..ba9fdc14b 100644 --- a/webapp/store/notifications.js +++ b/webapp/store/notifications.js @@ -1,5 +1,18 @@ +import gql from 'graphql-tag' export const actions = { - async markAsRead(_, notificationId) { - console.log('notificationId', notificationId) + async markAsRead({ commit, rootGetters }, notificationId) { + const client = this.app.apolloProvider.defaultClient + const mutation = gql(` + mutation($id: ID!, $read: Boolean!) { + UpdateNotification(id: $id, read: $read) { + id + read + } + } + `) + const variables = { id: notificationId, read: true } + const { + data: { UpdateNotification } + } = await client.mutate({ mutation, variables }) } } From 3559cfd9a3a07cbbc97b8462d8172ecec39ab878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 9 Apr 2019 15:09:38 +0200 Subject: [PATCH 14/91] Intermediate store/notifications --- webapp/store/notifications.js | 71 +++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/webapp/store/notifications.js b/webapp/store/notifications.js index ba9fdc14b..cfb41e333 100644 --- a/webapp/store/notifications.js +++ b/webapp/store/notifications.js @@ -1,5 +1,75 @@ import gql from 'graphql-tag' + +export const state = () => { + return { + notifications: null, + pending: false + } +} + +export const mutations = { + SET_NOTIFICATIONS(state, notifications) { + state.notifications = notifications + }, + SET_PENDING(state, pending) { + state.pending = pending + }, + UPDATE_NOTIFICATIONS(state, notification) { + const notifications = state.notifications + const toBeUpdated = notifications.find(n => { + return n.id === notification.id + }) + toBeUpdated = { ...toBeUpdated, ...notification } + } +} +export const getters = { + notifications(state) { + return !!state.notifications + } +} + export const actions = { + async init({ getters, commit }) { + if (getters.notifications) return + commit('SET_PENDING', true) + const client = this.app.apolloProvider.defaultClient + let notifications + try { + const { + data: { currentUser } + } = await client.query({ + query: gql(`{ + currentUser { + id + notifications(orderBy: createdAt_desc) { + id + read + createdAt + post { + author { + id + slug + name + disabled + deleted + } + title + contentExcerpt + slug + } + } + } + }`) + }) + notifications = currentUser.notifications + console.log(notifications) + commit('SET_NOTIFICATIONS', notifications) + } finally { + commit('SET_PENDING', false) + } + return notifications + }, + async markAsRead({ commit, rootGetters }, notificationId) { const client = this.app.apolloProvider.defaultClient const mutation = gql(` @@ -14,5 +84,6 @@ export const actions = { const { data: { UpdateNotification } } = await client.mutate({ mutation, variables }) + commit('UPDATE_NOTIFICATIONS', UpdateNotification) } } From 352b5ac7a405cee582e1cebd6fbe75479162f0eb 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 15/91] 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 cf27f9e8361917b9705460dc824b0e1df08908d0 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 16/91] 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 @@ @@ -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 24/91] 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 25/91] 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 26/91] 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 27/91] 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 @@ diff --git a/webapp/components/notifications/Notification/index.vue b/webapp/components/notifications/Notification/index.vue index ecf696d75..55b889876 100644 --- a/webapp/components/notifications/Notification/index.vue +++ b/webapp/components/notifications/Notification/index.vue @@ -1,21 +1,48 @@ + + diff --git a/webapp/components/notifications/Notification/spec.js b/webapp/components/notifications/Notification/spec.js index c55ea7bdc..8c6c846a4 100644 --- a/webapp/components/notifications/Notification/spec.js +++ b/webapp/components/notifications/Notification/spec.js @@ -46,5 +46,19 @@ describe('Notification', () => { it('renders title', () => { expect(Wrapper().text()).toContain("It's a title") }) + + it('has no class "read"', () => { + expect(Wrapper().classes()).not.toContain('read') + }) + + describe('that is read', () => { + beforeEach(() => { + propsData.notification.read = true + }) + + it('has class "read"', () => { + expect(Wrapper().classes()).toContain('read') + }) + }) }) })