import { config, mount } from '@vue/test-utils' import ContributionForm from './ContributionForm.vue' import Vue from 'vue' import Vuex from 'vuex' import PostMutations from '~/graphql/PostMutations.js' import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect' import ImageUploader from '~/components/ImageUploader/ImageUploader' import MutationObserver from 'mutation-observer' global.MutationObserver = MutationObserver const localVue = global.localVue config.stubs['client-only'] = '' config.stubs['nuxt-link'] = '' config.stubs['v-popover'] = '' const categories = [ { id: 'cat3', slug: 'health-wellbeing', icon: 'medkit', }, { id: 'cat12', slug: 'it-internet-data-privacy', icon: 'mouse-pointer', }, { id: 'cat9', slug: 'democracy-politics', icon: 'university', }, { id: 'cat15', slug: 'consumption-sustainability', icon: 'shopping-cart', }, { id: 'cat4', slug: 'environment-nature', icon: 'tree', }, ] describe('ContributionForm.vue', () => { let wrapper, postTitleInput, expectedParams, cancelBtn, mocks, propsData, categoryIds, englishLanguage, deutschLanguage, dataPrivacyButton const postTitle = 'this is a title for a post' const postTitleTooShort = 'xx' let postTitleTooLong = '' for (let i = 0; i < 101; i++) { postTitleTooLong += 'x' } const postContent = 'this is a post' const imageUpload = { file: { filename: 'avataar.svg', previewElement: '', }, url: 'someUrlToImage', } const image = '/uploads/1562010976466-avataaars' beforeEach(() => { mocks = { $t: jest.fn(), $apollo: { mutate: jest.fn().mockResolvedValueOnce({ data: { CreatePost: { title: postTitle, slug: 'this-is-a-title-for-a-post', content: postContent, contentExcerpt: postContent, language: 'en', categoryIds, }, }, }), }, $toast: { error: jest.fn(), success: jest.fn(), }, $i18n: { locale: () => 'en', }, $router: { back: jest.fn(), push: jest.fn(), }, } propsData = {} }) describe('mount', () => { const getters = { 'editor/placeholder': () => { return 'some cool placeholder' }, 'auth/isModerator': () => false, 'auth/user': () => { return { id: '4711', name: 'You yourself', slug: 'you-yourself', } }, } const store = new Vuex.Store({ getters, }) const Wrapper = () => { return mount(ContributionForm, { mocks, localVue, store, propsData, }) } beforeEach(() => { wrapper = Wrapper() }) describe('CreatePost', () => { describe('invalid form submission', () => { beforeEach(async () => { wrapper.find(CategoriesSelect).setData({ categories }) postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English') englishLanguage.trigger('click') dataPrivacyButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat12"]') dataPrivacyButton.trigger('click') }) it('title cannot be empty', async () => { postTitleInput.setValue('') wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('title cannot be too long', async () => { postTitleInput.setValue(postTitleTooLong) wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('title cannot be too short', async () => { postTitleInput.setValue(postTitleTooShort) wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('content cannot be empty', async () => { await wrapper.vm.updateEditorContent('') await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('has at least one category', async () => { dataPrivacyButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat12"]') dataPrivacyButton.trigger('click') wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('has no more than three categories', async () => { wrapper.vm.formData.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27'] await Vue.nextTick() wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) }) describe('valid form submission', () => { beforeEach(async () => { expectedParams = { mutation: PostMutations().CreatePost, variables: { title: postTitle, content: postContent, language: 'en', id: null, categoryIds: ['cat12'], imageUpload: null, imageAspectRatio: null, image: null, imageBlurred: false, }, } postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) wrapper.find(CategoriesSelect).setData({ categories }) englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English') englishLanguage.trigger('click') await Vue.nextTick() dataPrivacyButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat12"]') dataPrivacyButton.trigger('click') await Vue.nextTick() }) it('creates a post with valid title, content, and at least one category', async () => { await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) it('supports changing the language', async () => { expectedParams.variables.language = 'de' deutschLanguage = wrapper.findAll('li').filter(language => language.text() === 'Deutsch') deutschLanguage.trigger('click') wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) it('supports adding a teaser image', async () => { const spy = jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {}) expectedParams.variables.imageUpload = imageUpload wrapper.find(ImageUploader).vm.$emit('addHeroImage', imageUpload) await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(spy).toHaveBeenCalledWith(imageUpload) spy.mockReset() }) it('content is valid with just a link', async () => { await wrapper.vm.updateEditorContent( '', ) wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) }) it("pushes the user to the post's page", async () => { wrapper.find('form').trigger('submit') await mocks.$apollo.mutate expect(mocks.$router.push).toHaveBeenCalledTimes(1) }) it('shows a success toaster', async () => { wrapper.find('form').trigger('submit') await mocks.$apollo.mutate expect(mocks.$toast.success).toHaveBeenCalledTimes(1) }) }) describe('cancel', () => { it('calls $router.back() when cancel button clicked', () => { cancelBtn = wrapper.find('[data-test="cancel-button"]') cancelBtn.trigger('click') expect(mocks.$router.back).toHaveBeenCalledTimes(1) }) }) describe('handles errors', () => { beforeEach(async () => { jest.useFakeTimers() mocks.$apollo.mutate = jest.fn().mockRejectedValueOnce({ message: 'Not Authorised!', }) wrapper = Wrapper() postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) categoryIds = ['cat12'] wrapper.find(CategoriesSelect).setData({ categories }) englishLanguage = wrapper.findAll('li').filter(language => language.text() === 'English') englishLanguage.trigger('click') await Vue.nextTick() dataPrivacyButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat12"]') dataPrivacyButton.trigger('click') await Vue.nextTick() }) it('shows an error toaster when apollo mutation rejects', async () => { await wrapper.find('form').trigger('submit') await mocks.$apollo.mutate await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!') }) }) }) describe('UpdatePost', () => { beforeEach(() => { propsData = { contribution: { id: 'p1456', slug: 'dies-ist-ein-post', title: 'dies ist ein Post', content: 'auf Deutsch geschrieben', language: 'de', image, categories: [ { id: 'cat12', name: 'Democracy & Politics', }, ], imageAspectRatio: 1, }, } wrapper = Wrapper() }) it('sets title equal to contribution title', () => { expect(wrapper.vm.formData.title).toEqual(propsData.contribution.title) }) it('sets content equal to contribution content', () => { expect(wrapper.vm.formData.content).toEqual(propsData.contribution.content) }) describe('valid update', () => { beforeEach(() => { mocks.$apollo.mutate = jest.fn().mockResolvedValueOnce({ data: { UpdatePost: { title: postTitle, slug: 'this-is-a-title-for-a-post', content: postContent, contentExcerpt: postContent, language: 'en', categoryIds, }, }, }) wrapper = Wrapper() expectedParams = { mutation: PostMutations().UpdatePost, variables: { title: propsData.contribution.title, content: propsData.contribution.content, language: propsData.contribution.language, id: propsData.contribution.id, categoryIds: ['cat12'], image, imageUpload: null, imageAspectRatio: 1, imageBlurred: false, }, } }) it('calls the UpdatePost apollo mutation', async () => { expectedParams.variables.content = postContent wrapper.vm.updateEditorContent(postContent) await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) it('supports updating categories', async () => { expectedParams.variables.categoryIds.push('cat3') wrapper.find(CategoriesSelect).setData({ categories }) await Vue.nextTick() const healthWellbeingButton = await wrapper .find(CategoriesSelect) .find('[data-test="category-buttons-cat3"]') healthWellbeingButton.trigger('click') await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) it('supports deleting a teaser image', async () => { expectedParams.variables.image = null expectedParams.variables.imageAspectRatio = null propsData.contribution.image = '/uploads/someimage.png' wrapper = Wrapper() wrapper.find('[data-test="delete-button"]').trigger('click') await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) }) }) }) })