diff --git a/webapp/components/Comment/Comment.vue b/webapp/components/Comment/Comment.vue
index 06816e5bf..cc7f815b9 100644
--- a/webapp/components/Comment/Comment.vue
+++ b/webapp/components/Comment/Comment.vue
@@ -62,7 +62,7 @@
import { mapGetters } from 'vuex'
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
import HcUser from '~/components/User/User'
-import ContentMenu from '~/components/ContentMenu'
+import ContentMenu from '~/components/ContentMenu/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations'
diff --git a/webapp/components/ContentMenu/ContentMenu.spec.js b/webapp/components/ContentMenu/ContentMenu.spec.js
new file mode 100644
index 000000000..485c43145
--- /dev/null
+++ b/webapp/components/ContentMenu/ContentMenu.spec.js
@@ -0,0 +1,463 @@
+import { config, mount, createLocalVue } from '@vue/test-utils'
+import Vuex from 'vuex'
+import VTooltip from 'v-tooltip'
+import Styleguide from '@human-connection/styleguide'
+import ContentMenu from './ContentMenu.vue'
+
+const localVue = createLocalVue()
+
+localVue.use(Styleguide)
+localVue.use(VTooltip)
+localVue.use(Vuex)
+
+config.stubs['router-link'] = ''
+
+let getters, mutations, mocks, menuToggle, openModalSpy
+
+describe('ContentMenu.vue', () => {
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(str => str),
+ $i18n: {
+ locale: () => 'en',
+ },
+ $router: {
+ resolve: jest.fn(obj => {
+ obj.href = '/post/edit/d23a4265-f5f7-4e17-9f86-85f714b4b9f8'
+ return obj
+ }),
+ push: jest.fn(),
+ },
+ }
+ })
+
+ describe('mount', () => {
+ mutations = {
+ 'modal/SET_OPEN': jest.fn(),
+ }
+ getters = {
+ 'auth/isModerator': () => false,
+ 'auth/isAdmin': () => false,
+ }
+
+ const openContentMenu = (values = {}) => {
+ const store = new Vuex.Store({ mutations, getters })
+ const wrapper = mount(ContentMenu, {
+ propsData: {
+ ...values,
+ },
+ mocks,
+ store,
+ localVue,
+ })
+ menuToggle = wrapper.find('.content-menu-trigger')
+ menuToggle.trigger('click')
+ return wrapper
+ }
+
+ describe('owner of contribution', () => {
+ let wrapper
+ beforeEach(() => {
+ wrapper = openContentMenu({
+ isOwner: true,
+ resourceType: 'contribution',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ })
+
+ it('can edit the contribution', () => {
+ expect(
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'post.menu.edit')
+ .at(0)
+ .find('span.ds-menu-item-link')
+ .attributes('to'),
+ ).toBe('/post/edit/d23a4265-f5f7-4e17-9f86-85f714b4b9f8')
+ })
+
+ it('can delete the contribution', () => {
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'post.menu.delete')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('delete')
+ })
+ })
+
+ describe('admin can', () => {
+ it('pin unpinned post', () => {
+ getters['auth/isAdmin'] = () => true
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'contribution',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ pinnedBy: null,
+ },
+ })
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'post.menu.pin')
+ .at(0)
+ .trigger('click')
+ expect(wrapper.emitted('pinPost')).toEqual([
+ [
+ {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ pinnedBy: null,
+ },
+ ],
+ ])
+ })
+
+ it('unpin pinned post', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'contribution',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ pinnedBy: 'someone',
+ },
+ })
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'post.menu.unpin')
+ .at(0)
+ .trigger('click')
+ expect(wrapper.emitted('unpinPost')).toEqual([
+ [
+ {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ pinnedBy: 'someone',
+ },
+ ],
+ ])
+ })
+ })
+
+ describe('owner of comment can', () => {
+ let wrapper
+ beforeEach(() => {
+ wrapper = openContentMenu({
+ isOwner: true,
+ resourceType: 'comment',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ })
+ it('edit the comment', () => {
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'comment.menu.edit')
+ .at(0)
+ .trigger('click')
+ expect(wrapper.emitted('showEditCommentMenu')).toEqual([[true]])
+ })
+ it('delete the comment', () => {
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'comment.menu.delete')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('delete')
+ })
+ })
+
+ describe('reporting', () => {
+ it('a post of another user is possible', () => {
+ getters['auth/isAdmin'] = () => false
+ getters['auth/isModerator'] = () => false
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'contribution',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'report.contribution.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('report')
+ })
+
+ it('a comment of another user is possible', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'comment',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'report.comment.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('report')
+ })
+
+ it('another user is possible', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'user',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'report.user.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('report')
+ })
+
+ it('another organization is possible', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'organization',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'report.organization.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('report')
+ })
+ })
+
+ describe('moderator', () => {
+ it('can disable posts', () => {
+ getters['auth/isAdmin'] = () => false
+ getters['auth/isModerator'] = () => true
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'contribution',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: false,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'disable.contribution.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('disable')
+ })
+
+ it('can disable comments', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'comment',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: false,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'disable.comment.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('disable')
+ })
+
+ it('can disable users', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'user',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: false,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'disable.user.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('disable')
+ })
+
+ it('can disable organizations', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'organization',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: false,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'disable.organization.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('disable')
+ })
+
+ it('can release posts', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'contribution',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: true,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'release.contribution.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
+ })
+
+ it('can release comments', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'comment',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: true,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'release.comment.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
+ })
+
+ it('can release users', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'user',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: true,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'release.user.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
+ })
+
+ it('can release organizations', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'organization',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ disabled: true,
+ },
+ })
+ openModalSpy = jest.spyOn(wrapper.vm, 'openModal')
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'release.organization.title')
+ .at(0)
+ .trigger('click')
+ expect(openModalSpy).toHaveBeenCalledWith('release', 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8')
+ })
+ })
+
+ describe('user', () => {
+ it('can access settings', () => {
+ getters['auth/isAdmin'] = () => false
+ getters['auth/isModerator'] = () => false
+ const wrapper = openContentMenu({
+ isOwner: true,
+ resourceType: 'user',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ },
+ })
+ expect(
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'settings.name')
+ .at(0)
+ .find('span.ds-menu-item-link')
+ .attributes('to'),
+ ).toBe('/settings')
+ })
+
+ it('can block other users', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'user',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ isBlocked: false,
+ },
+ })
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'settings.blocked-users.block')
+ .at(0)
+ .trigger('click')
+ expect(wrapper.emitted('block')).toEqual([
+ [
+ {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ isBlocked: false,
+ },
+ ],
+ ])
+ })
+
+ it('can unblock blocked users', () => {
+ const wrapper = openContentMenu({
+ isOwner: false,
+ resourceType: 'user',
+ resource: {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ isBlocked: true,
+ },
+ })
+ wrapper
+ .findAll('.ds-menu-item')
+ .filter(item => item.text() === 'settings.blocked-users.unblock')
+ .at(0)
+ .trigger('click')
+ expect(wrapper.emitted('unblock')).toEqual([
+ [
+ {
+ id: 'd23a4265-f5f7-4e17-9f86-85f714b4b9f8',
+ isBlocked: true,
+ },
+ ],
+ ])
+ })
+ })
+ })
+})
diff --git a/webapp/components/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue
similarity index 100%
rename from webapp/components/ContentMenu.vue
rename to webapp/components/ContentMenu/ContentMenu.vue
diff --git a/webapp/components/PostCard/PostCard.vue b/webapp/components/PostCard/PostCard.vue
index 0947ed05b..1d4aa464f 100644
--- a/webapp/components/PostCard/PostCard.vue
+++ b/webapp/components/PostCard/PostCard.vue
@@ -74,7 +74,7 @@