diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 493c54e38..c51c863d1 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -5,9 +5,12 @@ import { } from "cypress-cucumber-preprocessor/steps"; import helpers from "../../support/helpers"; import { VERSION } from '../../constants/terms-and-conditions-version.js' +import locales from '../../../webapp/locales' +import orderBy from 'lodash/orderBy' /* global cy */ +const languages = orderBy(locales, 'name') let lastPost = {}; let loginCredentials = { @@ -245,6 +248,12 @@ Then("I select a category", () => { .click(); }); +When("I choose {string} as the language for the post", (languageCode) => { + cy.get('.ds-flex-item > .ds-form-item .ds-select ') + .click().get('.ds-select-option') + .eq(languages.findIndex(l => l.code === languageCode)).click() +}) + Then("the post shows up on the landing page at position {int}", index => { cy.openPage("landing"); const selector = `.post-card:nth-child(${index}) > .ds-card-content`; @@ -536,4 +545,4 @@ Then("I see only one post with the title {string}", title => { .find(".post-link") .should("have.length", 1); cy.get(".main-container").contains(".post-link", title); -}); \ No newline at end of file +}); diff --git a/cypress/integration/notifications/Mentions.feature b/cypress/integration/notifications/Mentions.feature index 7523e3d05..ef2694abc 100644 --- a/cypress/integration/notifications/Mentions.feature +++ b/cypress/integration/notifications/Mentions.feature @@ -20,6 +20,7 @@ Feature: Notification for a mention """ And mention "@matt-rider" in the text And I select a category + And I choose "en" as the language for the post And I click on "Save" When I log out And I log in with the following credentials: diff --git a/cypress/integration/post/WritePost.feature b/cypress/integration/post/WritePost.feature index 461766532..0d74606ad 100644 --- a/cypress/integration/post/WritePost.feature +++ b/cypress/integration/post/WritePost.feature @@ -17,7 +17,8 @@ Feature: Create a post Human Connection is a free and open-source social network for active citizenship. """ - Then I select a category + And I select a category + And I choose "en" as the language for the post And I click on "Save" Then I get redirected to ".../my-first-post" And the post was saved successfully diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.spec.js b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js index 199dacb74..56534c6ad 100644 --- a/webapp/components/CategoriesSelect/CategoriesSelect.spec.js +++ b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js @@ -8,10 +8,12 @@ localVue.use(Styleguide) describe('CategoriesSelect.vue', () => { let wrapper let mocks + let provide let democracyAndPolitics let environmentAndNature let consumptionAndSustainablity + const propsData = { model: 'categoryIds' } const categories = [ { id: 'cat9', @@ -35,6 +37,11 @@ describe('CategoriesSelect.vue', () => { }, ] beforeEach(() => { + provide = { + $parentForm: { + update: jest.fn(), + }, + } mocks = { $t: jest.fn(), } @@ -42,7 +49,7 @@ describe('CategoriesSelect.vue', () => { describe('shallowMount', () => { const Wrapper = () => { - return mount(CategoriesSelect, { mocks, localVue }) + return mount(CategoriesSelect, { propsData, mocks, localVue, provide }) } beforeEach(() => { @@ -60,8 +67,8 @@ describe('CategoriesSelect.vue', () => { expect(wrapper.vm.selectedCategoryIds).toEqual([categories[0].id]) }) - it('emits an updateCategories event when the selectedCategoryIds changes', () => { - expect(wrapper.emitted().updateCategories[0][0]).toEqual([categories[0].id]) + it('calls $parent.update with selected category ids', () => { + expect(provide.$parentForm.update).toHaveBeenCalledWith('categoryIds', ['cat9']) }) it('removes categories when clicked a second time', () => { diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.vue b/webapp/components/CategoriesSelect/CategoriesSelect.vue index cbe46b890..1fdb3ce0a 100644 --- a/webapp/components/CategoriesSelect/CategoriesSelect.vue +++ b/webapp/components/CategoriesSelect/CategoriesSelect.vue @@ -5,6 +5,7 @@ import CategoryQuery from '~/graphql/CategoryQuery' +import xor from 'lodash/xor' export default { + inject: { + $parentForm: { + default: null, + }, + }, props: { existingCategoryIds: { type: Array, default: () => [] }, + model: { type: String, required: true }, }, data() { return { categories: null, selectedMax: 3, - selectedCategoryIds: [], + selectedCategoryIds: this.existingCategoryIds, } }, computed: { @@ -48,39 +56,22 @@ export default { return this.selectedCount >= this.selectedMax }, }, - watch: { - selectedCategoryIds(categoryIds) { - this.$emit('updateCategories', categoryIds) - }, - existingCategoryIds: { - immediate: true, - handler: function(existingCategoryIds) { - if (!existingCategoryIds || !existingCategoryIds.length) { - return - } - this.selectedCategoryIds = existingCategoryIds - }, - }, - }, methods: { toggleCategory(id) { - const index = this.selectedCategoryIds.indexOf(id) - if (index > -1) { - this.selectedCategoryIds.splice(index, 1) - } else { - this.selectedCategoryIds.push(id) + this.selectedCategoryIds = xor(this.selectedCategoryIds, [id]) + if (this.$parentForm) { + this.$parentForm.update(this.model, this.selectedCategoryIds) } }, isActive(id) { - const index = this.selectedCategoryIds.indexOf(id) - if (index > -1) { - return true - } - return false + return this.selectedCategoryIds.includes(id) }, isDisabled(id) { return !!(this.reachedMaximum && !this.isActive(id)) }, + categoryButtonsId(categoryId) { + return `category-buttons-${categoryId}` + }, }, apollo: { Category: { diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index 836fe3fd3..52f77b3f2 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -20,15 +20,45 @@ 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 - let postTitleInput - let expectedParams - let deutschOption - let cancelBtn - let mocks - let propsData - let categoryIds + 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 = '' @@ -36,11 +66,6 @@ describe('ContributionForm.vue', () => { postTitleTooLong += 'x' } const postContent = 'this is a post' - const postContentTooShort = 'xx' - let postContentTooLong = '' - for (let i = 0; i < 2001; i++) { - postContentTooLong += 'x' - } const imageUpload = { file: { filename: 'avataar.svg', @@ -109,90 +134,59 @@ describe('ContributionForm.vue', () => { beforeEach(() => { wrapper = Wrapper() - wrapper.setData({ - form: { - languageOptions: [ - { - label: 'Deutsch', - value: 'de', - }, - ], - }, - }) }) describe('CreatePost', () => { - describe('language placeholder', () => { - it("displays the name that corresponds with the user's location code", () => { - expect(wrapper.find('.ds-select-placeholder').text()).toEqual('English') - }) - }) - describe('invalid form submission', () => { - it('title and content should not be empty ', async () => { - wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).not.toHaveBeenCalled() + 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 should not be empty', async () => { - await wrapper.vm.updateEditorContent(postContent) - wrapper.find('.submit-button-for-test').trigger('click') + postTitleInput.setValue('') + wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('title should not be too long', async () => { - postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitleTooLong) - await wrapper.vm.updateEditorContent(postContent) - wrapper.find('.submit-button-for-test').trigger('click') + wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('title should not be too short', async () => { - postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitleTooShort) - await wrapper.vm.updateEditorContent(postContent) - wrapper.find('.submit-button-for-test').trigger('click') + wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('content should not be empty', async () => { - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - await wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).not.toHaveBeenCalled() - }) - - it('content should not be too short', async () => { - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - await wrapper.vm.updateEditorContent(postContentTooShort) - wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).not.toHaveBeenCalled() - }) - - it('content should not be too long', async () => { - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - await wrapper.vm.updateEditorContent(postContentTooLong) - wrapper.find('.submit-button-for-test').trigger('click') + await wrapper.vm.updateEditorContent('') + await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) it('should have at least one category', async () => { - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - await wrapper.vm.updateEditorContent(postContent) - wrapper.find('.submit-button-for-test').trigger('click') + 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('should have not have more than three categories', async () => { - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - await wrapper.vm.updateEditorContent(postContent) wrapper.vm.form.categoryIds = ['cat4', 'cat9', 'cat15', 'cat27'] - wrapper.find('.submit-button-for-test').trigger('click') + wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).not.toHaveBeenCalled() }) }) @@ -214,43 +208,51 @@ describe('ContributionForm.vue', () => { postTitleInput = wrapper.find('.ds-input') postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) - categoryIds = ['cat12'] - wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) + wrapper.find(CategoriesSelect).setData({ categories }) + 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('creates a post with valid title, content, and at least one category', async () => { - await wrapper.find('.submit-button-for-test').trigger('click') - expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) - }) - - it("sends a fallback language based on a user's locale", () => { - wrapper.find('.submit-button-for-test').trigger('click') + await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) it('supports changing the language', async () => { expectedParams.variables.language = 'de' - deutschOption = wrapper.findAll('li').at(0) - deutschOption.trigger('click') - wrapper.find('.submit-button-for-test').trigger('click') + 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 () => { expectedParams.variables.imageUpload = imageUpload wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload) - await wrapper.find('.submit-button-for-test').trigger('click') + await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) + 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('.submit-button-for-test').trigger('click') + wrapper.find('form').trigger('submit') await mocks.$apollo.mutate expect(mocks.$router.push).toHaveBeenCalledTimes(1) }) it('shows a success toaster', async () => { - wrapper.find('.submit-button-for-test').trigger('click') + wrapper.find('form').trigger('submit') await mocks.$apollo.mutate expect(mocks.$toast.success).toHaveBeenCalledTimes(1) }) @@ -275,11 +277,17 @@ describe('ContributionForm.vue', () => { postTitleInput.setValue(postTitle) await wrapper.vm.updateEditorContent(postContent) categoryIds = ['cat12'] - wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) + wrapper.find(CategoriesSelect).setData({ categories }) + 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('shows an error toaster when apollo mutation rejects', async () => { - await wrapper.find('.submit-button-for-test').trigger('click') + await wrapper.find('form').trigger('submit') await mocks.$apollo.mutate await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!') }) @@ -341,11 +349,11 @@ describe('ContributionForm.vue', () => { expectedParams = { mutation: PostMutations().UpdatePost, variables: { - title: postTitle, - content: postContent, + title: propsData.contribution.title, + content: propsData.contribution.content, language: propsData.contribution.language, id: propsData.contribution.id, - categoryIds, + categoryIds: ['cat12'], image, imageUpload: null, }, @@ -353,22 +361,20 @@ describe('ContributionForm.vue', () => { }) it('calls the UpdatePost apollo mutation', async () => { - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) + expectedParams.variables.content = postContent wrapper.vm.updateEditorContent(postContent) - wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) - wrapper.find('.submit-button-for-test').trigger('click') + await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) }) it('supports updating categories', async () => { - const categoryIds = ['cat3', 'cat51', 'cat37'] - postTitleInput = wrapper.find('.ds-input') - postTitleInput.setValue(postTitle) - wrapper.vm.updateEditorContent(postContent) - expectedParams.variables.categoryIds = categoryIds - wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds) - await wrapper.find('.submit-button-for-test').trigger('click') + expectedParams.variables.categoryIds.push('cat3') + wrapper.find(CategoriesSelect).setData({ categories }) + 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)) }) }) diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index f5a02e305..734e3be81 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -1,5 +1,11 @@