diff --git a/backend/src/seed/factories/badges.js b/backend/src/seed/factories/badges.js
index e24a67c21..6f5f8d69a 100644
--- a/backend/src/seed/factories/badges.js
+++ b/backend/src/seed/factories/badges.js
@@ -3,21 +3,26 @@ import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
- key,
+ key = '',
type = 'crowdfunding',
status = 'permanent',
- icon
+ icon = '/img/badges/indiegogo_en_panda.svg'
} = params
- return `
- mutation {
- CreateBadge(
- id: "${id}",
- key: "${key}",
- type: ${type},
- status: ${status},
- icon: "${icon}"
- ) { id }
+ return {
+ mutation: `
+ mutation(
+ $id: ID
+ $key: String!
+ $type: BadgeTypeEnum!
+ $status: BadgeStatusEnum!
+ $icon: String!
+ ) {
+ CreateBadge(id: $id, key: $key, type: $type, status: $status, icon: $icon) {
+ id
+ }
+ }
+ `,
+ variables: { id, key, type, status, icon }
}
- `
}
diff --git a/backend/src/seed/factories/categories.js b/backend/src/seed/factories/categories.js
index a4b448f4b..5c1b3ce10 100644
--- a/backend/src/seed/factories/categories.js
+++ b/backend/src/seed/factories/categories.js
@@ -8,14 +8,15 @@ export default function (params) {
icon
} = params
- return `
- mutation {
- CreateCategory(
- id: "${id}",
- name: "${name}",
- slug: "${slug}",
- icon: "${icon}"
- ) { id, name }
+ return {
+ mutation: `
+ mutation($id: ID, $name: String!, $slug: String, $icon: String!) {
+ CreateCategory(id: $id, name: $name, slug: $slug, icon: $icon) {
+ id
+ name
+ }
}
- `
+ `,
+ variables: { id, name, slug, icon }
+ }
}
diff --git a/backend/src/seed/factories/comments.js b/backend/src/seed/factories/comments.js
index 92dca5b14..9964d0559 100644
--- a/backend/src/seed/factories/comments.js
+++ b/backend/src/seed/factories/comments.js
@@ -7,19 +7,17 @@ export default function (params) {
content = [
faker.lorem.sentence(),
faker.lorem.sentence()
- ].join('. '),
- disabled = false,
- deleted = false
+ ].join('. ')
} = params
- return `
- mutation {
- CreateComment(
- id: "${id}",
- content: "${content}",
- disabled: ${disabled},
- deleted: ${deleted}
- ) { id }
- }
- `
+ return {
+ mutation: `
+ mutation($id: ID!, $content: String!) {
+ CreateComment(id: $id, content: $content) {
+ id
+ }
+ }
+ `,
+ variables: { id, content }
+ }
}
diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js
index 7c23226cb..a0cb310ab 100644
--- a/backend/src/seed/factories/index.js
+++ b/backend/src/seed/factories/index.js
@@ -71,8 +71,8 @@ export default function Factory (options = {}) {
return this
},
async create (node, properties) {
- const mutation = this.factories[node](properties)
- this.lastResponse = await this.graphQLClient.request(mutation)
+ const { mutation, variables } = this.factories[node](properties)
+ this.lastResponse = await this.graphQLClient.request(mutation, variables)
return this
},
async relate (node, relationship, properties) {
diff --git a/backend/src/seed/factories/notifications.js b/backend/src/seed/factories/notifications.js
index 2e2abdd55..f7797200f 100644
--- a/backend/src/seed/factories/notifications.js
+++ b/backend/src/seed/factories/notifications.js
@@ -6,12 +6,15 @@ export default function (params) {
read = false
} = params
- return `
- mutation {
- CreateNotification(
- id: "${id}",
- read: ${read},
- ) { id, read }
- }
- `
+ return {
+ mutation: `
+ mutation($id: ID, $read: Boolean) {
+ CreateNotification(id: $id, read: $read) {
+ id
+ read
+ }
+ }
+ `,
+ variables: { id, read }
+ }
}
diff --git a/backend/src/seed/factories/organizations.js b/backend/src/seed/factories/organizations.js
index e0b2e52d4..dd4100b26 100644
--- a/backend/src/seed/factories/organizations.js
+++ b/backend/src/seed/factories/organizations.js
@@ -5,20 +5,17 @@ export default function create (params) {
const {
id = uuid(),
name = faker.company.companyName(),
- description = faker.company.catchPhrase(),
- disabled = false,
- deleted = false
+ description = faker.company.catchPhrase()
} = params
- return `
- mutation {
- CreateOrganization(
- id: "${id}",
- name: "${name}",
- description: "${description}",
- disabled: ${disabled},
- deleted: ${deleted}
- ) { name }
- }
- `
+ return {
+ mutation: `
+ mutation($id: ID!, $name: String!, $description: String!) {
+ CreateOrganization(id: $id, name: $name, description: $description) {
+ name
+ }
+ }
+ `,
+ variables: { id, name, description }
+ }
}
diff --git a/backend/src/seed/factories/posts.js b/backend/src/seed/factories/posts.js
index 83d5844d7..cbc73dbf8 100644
--- a/backend/src/seed/factories/posts.js
+++ b/backend/src/seed/factories/posts.js
@@ -18,17 +18,31 @@ export default function (params) {
deleted = false
} = params
- return `
- mutation {
- CreatePost(
- id: "${id}",
- slug: "${slug}",
- title: "${title}",
- content: "${content}",
- image: "${image}",
- visibility: ${visibility},
- deleted: ${deleted}
- ) { title, content }
- }
- `
+ return {
+ mutation: `
+ mutation(
+ $id: ID!
+ $slug: String
+ $title: String!
+ $content: String!
+ $image: String
+ $visibility: VisibilityEnum
+ $deleted: Boolean
+ ) {
+ CreatePost(
+ id: $id
+ slug: $slug
+ title: $title
+ content: $content
+ image: $image
+ visibility: $visibility
+ deleted: $deleted
+ ) {
+ title
+ content
+ }
+ }
+ `,
+ variables: { id, slug, title, content, image, visibility, deleted }
+ }
}
diff --git a/backend/src/seed/factories/reports.js b/backend/src/seed/factories/reports.js
index ccf5299bd..130c20c37 100644
--- a/backend/src/seed/factories/reports.js
+++ b/backend/src/seed/factories/reports.js
@@ -6,15 +6,15 @@ export default function create (params) {
id
} = params
- return `
- mutation {
- report(
- description: "${description}",
- id: "${id}",
- ) {
- id,
- createdAt
+ return {
+ mutation: `
+ mutation($id: ID!, $description: String!) {
+ report(description: $description, id: $id) {
+ id
+ createdAt
+ }
}
- }
- `
+ `,
+ variables: { id, description }
+ }
}
diff --git a/backend/src/seed/factories/tags.js b/backend/src/seed/factories/tags.js
index c603c5629..558b68957 100644
--- a/backend/src/seed/factories/tags.js
+++ b/backend/src/seed/factories/tags.js
@@ -3,15 +3,17 @@ import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
- name
+ name = '#human-connection'
} = params
- return `
- mutation {
- CreateTag(
- id: "${id}",
- name: "${name}",
- ) { name }
- }
- `
+ return {
+ mutation: `
+ mutation($id: ID!, $name: String!) {
+ CreateTag(id: $id, name: $name) {
+ name
+ }
+ }
+ `,
+ variables: { id, name }
+ }
}
diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js
index 9fe957515..1bca0e243 100644
--- a/backend/src/seed/factories/users.js
+++ b/backend/src/seed/factories/users.js
@@ -10,34 +10,42 @@ export default function create (params) {
password = '1234',
role = 'user',
avatar = faker.internet.avatar(),
- about = faker.lorem.paragraph(),
- disabled = false,
- deleted = false
+ about = faker.lorem.paragraph()
} = params
- return `
- mutation {
- CreateUser(
- id: "${id}",
- name: "${name}",
- slug: "${slug}",
- password: "${password}",
- email: "${email}",
- avatar: "${avatar}",
- about: "${about}",
- role: ${role},
- disabled: ${disabled},
- deleted: ${deleted}
+ return {
+ mutation: `
+ mutation(
+ $id: ID!
+ $name: String
+ $slug: String
+ $password: String!
+ $email: String
+ $avatar: String
+ $about: String
+ $role: UserGroupEnum
) {
- id
- name
- slug
- email
- avatar
- role
- deleted
- disabled
+ CreateUser(
+ id: $id
+ name: $name
+ slug: $slug
+ password: $password
+ email: $email
+ avatar: $avatar
+ about: $about
+ role: $role
+ ) {
+ id
+ name
+ slug
+ email
+ avatar
+ role
+ deleted
+ disabled
+ }
}
- }
- `
+ `,
+ variables: { id, name, slug, password, email, avatar, about, role }
+ }
}
diff --git a/backend/src/seed/seed-db.js b/backend/src/seed/seed-db.js
index 3de70e643..149b461b1 100644
--- a/backend/src/seed/seed-db.js
+++ b/backend/src/seed/seed-db.js
@@ -1,3 +1,4 @@
+import faker from 'faker'
import Factory from './factories'
/* eslint-disable no-multi-spaces */
@@ -88,20 +89,23 @@ import Factory from './factories'
f.create('Tag', { id: 't4', name: 'Freiheit' })
])
+ const mention1 = 'Hey @jenny-rostock, what\'s up?'
+ const mention2 = 'Hey @jenny-rostock, here is another notification for you!'
+
await Promise.all([
asAdmin.create('Post', { id: 'p0' }),
asModerator.create('Post', { id: 'p1' }),
- asUser.create('Post', { id: 'p2', deleted: true }),
+ asUser.create('Post', { id: 'p2' }),
asTick.create('Post', { id: 'p3' }),
asTrick.create('Post', { id: 'p4' }),
asTrack.create('Post', { id: 'p5' }),
asAdmin.create('Post', { id: 'p6' }),
- asModerator.create('Post', { id: 'p7' }),
+ asModerator.create('Post', { id: 'p7', content: `${mention1} ${faker.lorem.paragraph()}` }),
asUser.create('Post', { id: 'p8' }),
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: `${mention2} ${faker.lorem.paragraph()}` }),
asModerator.create('Post', { id: 'p13' }),
asUser.create('Post', { id: 'p14' }),
asTick.create('Post', { id: 'p15' })
diff --git a/cypress/README.md b/cypress/README.md
index e55ff22af..92b1b8185 100644
--- a/cypress/README.md
+++ b/cypress/README.md
@@ -1,10 +1,23 @@
# End-to-End Testing
+## Configure cypress
+
+First, you have to tell cypress how to connect to your local neo4j database
+among other things. You can copy our template configuration and change the new
+file according to your needs.
+
+Make sure you are at the root level of the project. Then:
+```bash
+# in the top level folder Human-Connection/
+$ cp cypress.env.template.json cypress.env.json
+```
+
## Run Tests
-To run the tests, make sure you are at the root level of the project, in your console and run the following command:
+To run the tests, do this:
```bash
+# in the top level folder Human-Connection/
$ yarn cypress:setup
```
diff --git a/cypress/features.md b/cypress/features.md
index 345840e39..eb8292c3b 100644
--- a/cypress/features.md
+++ b/cypress/features.md
@@ -83,6 +83,13 @@ The following features will be implemented. This gets done in three steps:
* Editing Comments
* Upvote comments of others
+### Notifications
+[Cucumber features](./integration/notifications)
+
+* User @-mentionings
+* Notify authors for comments
+* Administrative notifications to all users
+
### Contribution List
* Show Posts by Tiles
diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js
index 9478d8d4e..85a660f43 100644
--- a/cypress/integration/common/steps.js
+++ b/cypress/integration/common/steps.js
@@ -293,3 +293,43 @@ 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', () => {
+ cy.get('.notifications-menu').click()
+ cy.get('.notification-mention-post').first().click()
+})
+
+Then('see {int} unread notifications in the top menu', count => {
+ cy.get('.notifications-menu').should('contain', count)
+})
+
+Then('I get to the post page of {string}', path => {
+ path = path.replace('...', '')
+ cy.url().should('contain', '/post/')
+ cy.url().should('contain', path)
+})
+
+When('I start to write a new post with the title {string} beginning with:', (title, intro) => {
+ cy.get('.post-add-button').click()
+ cy.get('input[name="title"]').type(title)
+ cy.get('.ProseMirror').type(intro)
+})
+
+When('mention {string} in the text', (mention) => {
+ cy.get('.ProseMirror').type(' @')
+ cy.get('.suggestion-list__item').contains(mention).click()
+ cy.debug()
+})
+
+Then('the notification gets marked as read', () => {
+ cy.get('.notification').first().should('have.class', 'read')
+})
+
+Then('there are no notifications in the top menu', () => {
+ cy.get('.notifications-menu').should('contain', '0')
+})
diff --git a/cypress/integration/notifications/Mentions.feature b/cypress/integration/notifications/Mentions.feature
new file mode 100644
index 000000000..28f7cf456
--- /dev/null
+++ b/cypress/integration/notifications/Mentions.feature
@@ -0,0 +1,31 @@
+Feature: Notifications for a mentions
+ 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 |
+ | Matt Rider | matt-rider | matt@example.org | 4321 |
+
+ Scenario: Mention another user, re-login as this user and see notifications
+ Given I log in with the following credentials:
+ | email | password |
+ | wolle@example.org | 1234 |
+ And I start to write a new post with the title "Hey Matt" beginning with:
+ """
+ Big shout to our fellow contributor
+ """
+ And mention "@matt-rider" in the text
+ And I click on "Save"
+ When I log out
+ And I log in with the following credentials:
+ | email | password |
+ | matt@example.org | 4321 |
+ 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-matt"
+ And the notification gets marked as read
+ But when I refresh the page
+ Then there are no notifications in the top menu
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 5b4c2055b..a7cb76a27 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -46,7 +46,8 @@ Cypress.Commands.add('login', ({ email, password }) => {
cy.get('button[name=submit]')
.as('submitButton')
.click()
- cy.location('pathname').should('eq', '/') // we're in!
+ cy.get('.iziToast-message').should('contain', 'You are logged in!')
+ cy.get('.iziToast-close').click()
})
Cypress.Commands.add('logout', (email, password) => {
diff --git a/webapp/components/PostCard.vue b/webapp/components/PostCard/index.vue
similarity index 80%
rename from webapp/components/PostCard.vue
rename to webapp/components/PostCard/index.vue
index 4bb1e4693..acddba5d6 100644
--- a/webapp/components/PostCard.vue
+++ b/webapp/components/PostCard/index.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 }}
+
@@ -75,6 +76,7 @@
import HcUser from '~/components/User'
import ContentMenu from '~/components/ContentMenu'
import { randomBytes } from 'crypto'
+import { mapGetters } from 'vuex'
export default {
name: 'HcPostCard',
@@ -89,26 +91,16 @@ export default {
}
},
computed: {
+ ...mapGetters({
+ user: 'auth/user'
+ }),
excerpt() {
- // remove all links from excerpt to prevent issues with the serounding link
- let excerpt = this.post.contentExcerpt.replace(/(.+)<\/a>/gim, '$1')
- // do not display content that is only linebreaks
- if (excerpt.replace(/
/gim, '').trim() === '') {
- excerpt = ''
- }
-
- return excerpt
+ return this.$filters.removeLinks(this.post.contentExcerpt)
},
isAuthor() {
- return this.$store.getters['auth/user'].id === this.post.author.id
- }
- },
- methods: {
- href(post) {
- return this.$router.resolve({
- name: 'post-id-slug',
- params: { id: post.id, slug: post.slug }
- }).href
+ const { author } = this.post
+ if (!author) return false
+ return this.user.id === this.post.author.id
}
}
}
@@ -130,6 +122,7 @@ export default {
}
.post-link {
+ margin: 15px;
display: block;
position: absolute;
top: 0;
diff --git a/webapp/components/PostCard/spec.js b/webapp/components/PostCard/spec.js
new file mode 100644
index 000000000..1914733c0
--- /dev/null
+++ b/webapp/components/PostCard/spec.js
@@ -0,0 +1,62 @@
+import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
+import PostCard from '.'
+import Styleguide from '@human-connection/styleguide'
+import Vuex from 'vuex'
+import Filters from '~/plugins/vue-filters'
+
+const localVue = createLocalVue()
+
+localVue.use(Vuex)
+localVue.use(Styleguide)
+localVue.use(Filters)
+
+config.stubs['no-ssr'] = ''
+config.stubs['v-popover'] = ''
+
+describe('PostCard', () => {
+ let wrapper
+ let stubs
+ let mocks
+ let propsData
+ let getters
+
+ beforeEach(() => {
+ propsData = {}
+ stubs = {
+ NuxtLink: RouterLinkStub
+ }
+ mocks = {
+ $t: jest.fn()
+ }
+ getters = {
+ 'auth/user': () => {
+ return {}
+ }
+ }
+ })
+
+ const Wrapper = () => {
+ const store = new Vuex.Store({
+ getters
+ })
+ return mount(PostCard, {
+ stubs,
+ mocks,
+ propsData,
+ store,
+ localVue
+ })
+ }
+
+ describe('given a post', () => {
+ beforeEach(() => {
+ propsData.post = {
+ title: "It's a title"
+ }
+ })
+
+ it('renders title', () => {
+ expect(Wrapper().text()).toContain("It's a title")
+ })
+ })
+})
diff --git a/webapp/components/notifications/Notification/index.vue b/webapp/components/notifications/Notification/index.vue
new file mode 100644
index 000000000..cae34a2f1
--- /dev/null
+++ b/webapp/components/notifications/Notification/index.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+ {{ $t("notifications.menu.mentioned") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/components/notifications/Notification/spec.js b/webapp/components/notifications/Notification/spec.js
new file mode 100644
index 000000000..8c6c846a4
--- /dev/null
+++ b/webapp/components/notifications/Notification/spec.js
@@ -0,0 +1,64 @@
+import { config, mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
+import Notification from '.'
+import Styleguide from '@human-connection/styleguide'
+import Filters from '~/plugins/vue-filters'
+
+const localVue = createLocalVue()
+
+localVue.use(Styleguide)
+localVue.use(Filters)
+
+config.stubs['no-ssr'] = ''
+
+describe('Notification', () => {
+ let wrapper
+ let stubs
+ let mocks
+ let propsData
+ beforeEach(() => {
+ propsData = {}
+ mocks = {
+ $t: jest.fn()
+ }
+ stubs = {
+ NuxtLink: RouterLinkStub
+ }
+ })
+
+ const Wrapper = () => {
+ return mount(Notification, {
+ stubs,
+ mocks,
+ propsData,
+ localVue
+ })
+ }
+
+ describe('given a notification', () => {
+ beforeEach(() => {
+ propsData.notification = {
+ post: {
+ title: "It's a title"
+ }
+ }
+ })
+
+ 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')
+ })
+ })
+ })
+})
diff --git a/webapp/components/notifications/NotificationList/index.vue b/webapp/components/notifications/NotificationList/index.vue
new file mode 100644
index 000000000..b8235f853
--- /dev/null
+++ b/webapp/components/notifications/NotificationList/index.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/webapp/components/notifications/NotificationList/spec.js b/webapp/components/notifications/NotificationList/spec.js
new file mode 100644
index 000000000..c433abd80
--- /dev/null
+++ b/webapp/components/notifications/NotificationList/spec.js
@@ -0,0 +1,130 @@
+import {
+ config,
+ shallowMount,
+ mount,
+ createLocalVue,
+ RouterLinkStub
+} from '@vue/test-utils'
+import NotificationList from '.'
+import Notification from '../Notification'
+import Vue from 'vue'
+import Vuex from 'vuex'
+import Filters from '~/plugins/vue-filters'
+
+import Styleguide from '@human-connection/styleguide'
+
+const localVue = createLocalVue()
+
+localVue.use(Vuex)
+localVue.use(Styleguide)
+localVue.use(Filters)
+localVue.filter('truncate', string => string)
+
+config.stubs['no-ssr'] = ''
+config.stubs['v-popover'] = ''
+
+describe('NotificationList.vue', () => {
+ let wrapper
+ let Wrapper
+ let mocks
+ let stubs
+ let store
+ let propsData
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ getters: {
+ 'auth/user': () => {
+ return {}
+ }
+ }
+ })
+ mocks = {
+ $t: jest.fn()
+ }
+ stubs = {
+ NuxtLink: RouterLinkStub
+ }
+ 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, {
+ propsData,
+ mocks,
+ store,
+ localVue
+ })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ it('renders Notification.vue for each notification of the user', () => {
+ expect(wrapper.findAll(Notification)).toHaveLength(2)
+ })
+ })
+
+ describe('mount', () => {
+ const Wrapper = () => {
+ return mount(NotificationList, {
+ propsData,
+ mocks,
+ stubs,
+ store,
+ localVue
+ })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ describe('click on a notification', () => {
+ beforeEach(() => {
+ wrapper
+ .findAll('.notification-mention-post')
+ .at(1)
+ .trigger('click')
+ })
+
+ 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/NotificationMenu/index.vue b/webapp/components/notifications/NotificationMenu/index.vue
new file mode 100644
index 000000000..819858e3b
--- /dev/null
+++ b/webapp/components/notifications/NotificationMenu/index.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
diff --git a/webapp/components/notifications/NotificationMenu/spec.js b/webapp/components/notifications/NotificationMenu/spec.js
new file mode 100644
index 000000000..4c9dd9d43
--- /dev/null
+++ b/webapp/components/notifications/NotificationMenu/spec.js
@@ -0,0 +1,94 @@
+import { config, shallowMount, createLocalVue } from '@vue/test-utils'
+import NotificationMenu from '.'
+
+import Styleguide from '@human-connection/styleguide'
+import Filters from '~/plugins/vue-filters'
+
+const localVue = createLocalVue()
+
+localVue.use(Styleguide)
+localVue.use(Filters)
+localVue.filter('truncate', string => string)
+
+config.stubs['dropdown'] = ''
+
+describe('NotificationMenu.vue', () => {
+ let wrapper
+ let Wrapper
+ let mocks
+ let data
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn()
+ }
+ data = () => {
+ return {
+ notifications: []
+ }
+ }
+ })
+
+ describe('shallowMount', () => {
+ const Wrapper = () => {
+ return shallowMount(NotificationMenu, {
+ data,
+ mocks,
+ localVue
+ })
+ }
+
+ it('counter displays 0', () => {
+ wrapper = Wrapper()
+ expect(wrapper.find('ds-button-stub').text()).toEqual('0')
+ })
+
+ it('no dropdown is rendered', () => {
+ wrapper = Wrapper()
+ expect(wrapper.contains('.dropdown')).toBe(false)
+ })
+
+ describe('given some notifications', () => {
+ beforeEach(() => {
+ 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'
+ }
+ }
+ }
+ ]
+ }
+ }
+ })
+
+ it('displays the total number of notifications', () => {
+ wrapper = Wrapper()
+ expect(wrapper.find('ds-button-stub').text()).toEqual('2')
+ })
+ })
+ })
+})
diff --git a/webapp/layouts/blank.vue b/webapp/layouts/blank.vue
index 140ec9f6e..570799a4d 100644
--- a/webapp/layouts/blank.vue
+++ b/webapp/layouts/blank.vue
@@ -10,7 +10,7 @@