diff --git a/.gitignore b/.gitignore index fbdc98903..7c4393cb7 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ static/uploads cypress/videos cypress/screenshots/ +cypress.env.json diff --git a/.travis.yml b/.travis.yml index f5cb1b4ef..2b1608b01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,22 +18,23 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin + - cp cypress.env.template.json cypress.env.json install: - docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-web . - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d - git clone https://github.com/Human-Connection/Nitro-Backend.git ../Nitro-Backend - git -C "../Nitro-Backend" checkout $BACKEND_BRANCH || git -C "../Nitro-Backend" checkout master - - docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.travis.yml up -d + - cd ../Nitro-Backend && yarn install && cd - + - docker-compose -f ../Nitro-Backend/docker-compose.yml -f ../Nitro-Backend/docker-compose.cypress.yml up -d - yarn global add cypress wait-on - yarn add cypress-cucumber-preprocessor script: - docker-compose exec -e NODE_ENV=test webapp yarn run lint - docker-compose exec -e NODE_ENV=test webapp yarn run test - - docker-compose -f ../Nitro-Backend/docker-compose.yml exec backend yarn run db:seed - - wait-on http://localhost:3000 - - cypress run --record --key $CYPRESS_TOKEN + - wait-on http://localhost:7474 && docker-compose -f ../Nitro-Backend/docker-compose.yml exec neo4j migrate + - wait-on http://localhost:3000 && cypress run --record --key $CYPRESS_TOKEN after_success: - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh diff --git a/README.md b/README.md index f7d296f87..45f6e9010 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,9 @@ $ yarn install Copy: ``` cp .env.template .env +cp cypress.env.template.json cypress.env.json ``` -Configure the file `.env` according to your needs and your local setup. +Configure the files according to your needs and your local setup. ### Development ``` bash diff --git a/assets/styles/main.scss b/assets/styles/main.scss index d22f3ec08..662b8bb0d 100644 --- a/assets/styles/main.scss +++ b/assets/styles/main.scss @@ -78,45 +78,6 @@ blockquote { } } -.hc-editor-content { - h1, - h2, - h3, - h4, - h5, - h6 { - &:not(:first-child) { - margin-top: 2rem; - } - } - p { - &:not(:last-child) { - margin-top: 0; - margin-bottom: 0; - } - } - dl, - ol, - ul, - blockquote, - pre, - table { - &:not(:first-child) { - margin-top: 15px; - } - } - *:first-child { - margin-top: 0; - } - *:last-child { - margin-bottom: 0; - } - // avoid double breaks - br + p { - margin-top: 0; - } -} - hr { border: 0; width: 100%; diff --git a/components/ContentMenu.vue b/components/ContentMenu.vue index 9ab85cb12..5e614c476 100644 --- a/components/ContentMenu.vue +++ b/components/ContentMenu.vue @@ -8,14 +8,19 @@ slot="default" slot-scope="{toggleMenu}" > - - - + + + +
{ + console.log('EDIT COMMENT') + }, + icon: 'edit' + }) + } + + if (!this.isOwner) { + routes.push({ name: this.$t(`report.${this.context}.title`), callback: this.openReportDialog, icon: 'flag' - } - ] - if (this.isModerator) { + }) + } + + if (!this.isOwner && this.isModerator) { routes.push({ name: this.$t(`disable.${this.context}.title`), - callback: this.openDisableDialog, + callback: () => {}, icon: 'eye-slash' }) } + + if (this.isOwner && this.context === 'user') { + routes.push({ + name: this.$t(`settings.data.name`), + // eslint-disable-next-line vue/no-side-effects-in-computed-properties + callback: () => this.$router.push('/settings'), + icon: 'edit' + }) + } return routes }, isModerator() { diff --git a/components/ContributionForm.vue b/components/ContributionForm.vue new file mode 100644 index 000000000..cf7c28ece --- /dev/null +++ b/components/ContributionForm.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/components/Editor/Editor.vue b/components/Editor/Editor.vue new file mode 100644 index 000000000..b59ca376d --- /dev/null +++ b/components/Editor/Editor.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/components/Editor/plugins/eventHandler.js b/components/Editor/plugins/eventHandler.js new file mode 100644 index 000000000..6086421b8 --- /dev/null +++ b/components/Editor/plugins/eventHandler.js @@ -0,0 +1,83 @@ +import { Extension, Plugin } from 'tiptap' +// import { Slice, Fragment } from 'prosemirror-model' + +export default class EventHandler extends Extension { + get name() { + return 'event_handler' + } + get plugins() { + return [ + new Plugin({ + props: { + transformPastedText(text) { + // console.log('#### transformPastedText', text) + return text.trim() + }, + transformPastedHTML(html) { + html = html + // remove all tags with "space only" + .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '') + // remove all iframes + .replace( + /(]*)(>)[^>]*\/*>/gim, + '' + ) + .replace(/[\n]{3,}/gim, '\n\n') + .replace(/(\r\n|\n\r|\r|\n)/g, '
$1') + + // replace all p tags with line breaks (and spaces) only by single linebreaks + // limit linebreaks to max 2 (equivalent to html "br" linebreak) + .replace(/(
\s*){2,}/gim, '
') + // remove additional linebreaks after p tags + .replace( + /<\/(p|div|th|tr)>\s*(
\s*)+\s*<(p|div|th|tr)>/gim, + '

' + ) + // remove additional linebreaks inside p tags + .replace( + /<[a-z-]+>(<[a-z-]+>)*\s*(
\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, + '' + ) + // remove additional linebreaks when first child inside p tags + .replace(/

(\s*
\s*)+/gim, '

') + // remove additional linebreaks when last child inside p tags + .replace(/(\s*
\s*)+<\/p>/gim, '

') + // console.log('#### transformPastedHTML', html) + return html + } + // transformPasted(slice) { + // // console.log('#### transformPasted', slice.content) + // let content = [] + // let size = 0 + // slice.content.forEach((node, offset, index) => { + // // console.log(node) + // // console.log('isBlock', node.type.isBlock) + // // console.log('childCount', node.content.childCount) + // // console.log('index', index) + // if (node.content.childCount) { + // content.push(node.content) + // size += node.content.size + // } + // }) + // console.log(content) + // console.log(slice.content) + // let fragment = Fragment.fromArray(content) + // fragment.size = size + // console.log('#fragment', fragment, slice.content) + // console.log('----') + // console.log('#1', slice) + // // const newSlice = new Slice(fragment, slice.openStart, slice.openEnd) + // slice.fragment = fragment + // // slice.content.content = fragment.content + // // slice.content.size = fragment.size + // console.log('#2', slice) + // // console.log(newSlice) + // console.log('----') + // return slice + // // return newSlice + // } + } + }) + ] + } +} diff --git a/components/PostCard.vue b/components/PostCard.vue index 25b410257..090517a37 100644 --- a/components/PostCard.vue +++ b/components/PostCard.vue @@ -55,6 +55,7 @@ context="contribution" :item-id="post.id" :name="post.title" + :is-owner="isAuthor" />
@@ -86,13 +87,16 @@ export default { computed: { excerpt() { // remove all links from excerpt to prevent issues with the serounding link - let excerpt = this.post.contentExcerpt.replace(/(.+)<\/a>/gim, '') + 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 + }, + isAuthor() { + return this.$store.getters['auth/user'].id === this.post.author.id } }, methods: { diff --git a/cypress.env.template.json b/cypress.env.template.json new file mode 100644 index 000000000..bd03f6381 --- /dev/null +++ b/cypress.env.template.json @@ -0,0 +1,6 @@ +{ + "SEED_SERVER_HOST": "http://localhost:4001", + "NEO4J_URI": "bolt://localhost:7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "letmein" +} diff --git a/cypress/integration/01.Login.feature b/cypress/integration/01.Login.feature index 72adc8553..3837f7042 100644 --- a/cypress/integration/01.Login.feature +++ b/cypress/integration/01.Login.feature @@ -4,11 +4,7 @@ Feature: Authentication In order to attribute posts and other contributions to their authors Background: - Given my account has the following details: - | name | email | password | type - | Peter Lustig | admin@example.org | 1234 | Admin - | Bob der Bausmeister | moderator@example.org | 1234 | Moderator - | Jenny Rostock" | user@example.org | 1234 | User + Given I have a user account Scenario: Log in When I visit the "/login" page diff --git a/cypress/integration/03.TagsAndCategories.feature b/cypress/integration/03.TagsAndCategories.feature index f9509e858..8f27a36cf 100644 --- a/cypress/integration/03.TagsAndCategories.feature +++ b/cypress/integration/03.TagsAndCategories.feature @@ -14,28 +14,27 @@ Feature: Tags and Categories looking at the popularity of a tag. Background: - Given we have a selection of tags and categories as well as posts - And my user account has the role "administrator" - Given I am logged in + Given my user account has the role "admin" + And we have a selection of tags and categories as well as posts + And I am logged in Scenario: See an overview of categories When I navigate to the administration dashboard And I click on "Categories" Then I can see a list of categories ordered by post count: - | Icon | Name | Post Count | - | | Just For Fun | 5 | - | | Happyness & Values | 2 | - | | Health & Wellbeing | 1 | + | Icon | Name | Posts | + | | Just For Fun | 2 | + | | Happyness & Values | 1 | + | | Health & Wellbeing | 0 | Scenario: See an overview of tags When I navigate to the administration dashboard And I click on "Tags" - Then I can see a list of tags ordered by user and post count: - | # | Name | Nutzer | Beiträge | - | 1 | Naturschutz | 2 | 2 | - | 2 | Freiheit | 2 | 2 | - | 3 | Umwelt | 1 | 1 | - | 4 | Demokratie | 1 | 1 | + Then I can see a list of tags ordered by user count: + | # | Name | Users | Posts | + | 1 | Democracy | 2 | 3 | + | 2 | Ecology | 1 | 1 | + | 3 | Nature | 1 | 2 | diff --git a/cypress/integration/04.AboutMeAndLocation.feature b/cypress/integration/04.AboutMeAndLocation.feature index 8601f7c80..2a512bf3f 100644 --- a/cypress/integration/04.AboutMeAndLocation.feature +++ b/cypress/integration/04.AboutMeAndLocation.feature @@ -7,21 +7,18 @@ Feature: About me and location to search for users by location. Background: - Given I am logged in + Given I have a user account + And I am logged in And I am on the "settings" page Scenario: Change username When I save "Hansi" as my new name Then I can see my new name "Hansi" when I click on my profile picture in the top right - - Scenario: Keep changes after refresh - When I changed my username to "Hansi" previously - And I refresh the page - Then my new username is still there + And when I refresh the page + Then the name "Hansi" is still there Scenario Outline: I set my location to "" When I save "" as my location - And my username is "Peter Lustig" When people visit my profile page Then they can see the location in the info box below my avatar @@ -36,10 +33,5 @@ Feature: About me and location """ Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei """ - And my username is "Peter Lustig" When people visit my profile page Then they can see the text in the info box below my avatar - - - - diff --git a/cypress/integration/05.ReportContent.feature b/cypress/integration/05.ReportContent.feature index fbfdfe8de..76ebb4fcd 100644 --- a/cypress/integration/05.ReportContent.feature +++ b/cypress/integration/05.ReportContent.feature @@ -9,13 +9,13 @@ Feature: Report and Moderate Background: Given we have the following posts in our database: - | Author | Title | Content | Slug | - | David Irving | The Truth about the Holocaust | It never existed! | the-truth-about-the-holocaust | + | Author | id | title | content | + | David Irving | p1 | The Truth about the Holocaust | It never existed! | Scenario Outline: Report a post from various pages Given I am logged in with a "user" role - And I see David Irving's post on the - When I click on "Report Contribution" from the triple dot menu of the post + When I see David Irving's post on the + And I click on "Report Contribution" from the triple dot menu of the post And I confirm the reporting dialog because it is a criminal act under German law: """ Do you really want to report the contribution "The Truth about the Holocaust"? @@ -45,8 +45,8 @@ Feature: Report and Moderate Scenario: Review reported content Given somebody reported the following posts: - | Slug | - | the-truth-about-the-holocaust | + | id | + | p1 | And I am logged in with a "moderator" role When I click on the avatar menu in the top right corner And I click on "Moderation" diff --git a/cypress/integration/06.WritePost.feature b/cypress/integration/06.WritePost.feature new file mode 100644 index 000000000..0193e44bf --- /dev/null +++ b/cypress/integration/06.WritePost.feature @@ -0,0 +1,25 @@ +Feature: Create a post + As a user + I would like to create a post + To say something to everyone in the community + + Background: + Given I have a user account + And I am logged in + And I am on the "landing" page + + Scenario: Create a post + When I click on the big plus icon in the bottom right corner to create post + And I choose "My first post" as the title of the post + And I type in the following text: + """ + Human Connection is a free and open-source social network + for active citizenship. + """ + And I click on "Save" + Then I get redirected to "/post/my-first-post/" + And the post was saved successfully + + Scenario: See a post on the landing page + Given I previously created a post + Then the post shows up on the landing page at position 1 diff --git a/cypress/integration/common/admin.js b/cypress/integration/common/admin.js index 9162667b4..03cbe5fca 100644 --- a/cypress/integration/common/admin.js +++ b/cypress/integration/common/admin.js @@ -2,18 +2,6 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps' /* global cy */ -const lastColumnIsSortedInDescendingOrder = () => { - cy.get('tbody') - .find('tr td:last-child') - .then(lastColumn => { - cy.wrap(lastColumn) - const values = lastColumn - .map((i, td) => parseInt(td.textContent)) - .toArray() - const orderedDescending = values.slice(0).sort((a, b) => b - a) - return cy.wrap(values).should('deep.eq', orderedDescending) - }) -} When('I navigate to the administration dashboard', () => { cy.get('.avatar-menu').click() @@ -23,17 +11,27 @@ When('I navigate to the administration dashboard', () => { }) Then('I can see a list of categories ordered by post count:', table => { - // TODO: match the table in the feature with the html table cy.get('thead') .find('tr th') .should('have.length', 3) - lastColumnIsSortedInDescendingOrder() + table.hashes().forEach(({Name, Posts}, index) => { + cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`) + .should('contain', Name) + cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`) + .should('contain', Posts) + }) }) -Then('I can see a list of tags ordered by user and post count:', table => { - // TODO: match the table in the feature with the html table +Then('I can see a list of tags ordered by user count:', table => { cy.get('thead') .find('tr th') .should('have.length', 4) - lastColumnIsSortedInDescendingOrder() + table.hashes().forEach(({Name, Users, Posts}, index) => { + cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(2)`) + .should('contain', Name) + cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(3)`) + .should('contain', Users) + cy.get(`tbody > :nth-child(${index + 1}) > :nth-child(4)`) + .should('contain', Posts) + }) }) diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 3c546f0f5..3f2895dd9 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -3,9 +3,9 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' /* global cy */ let lastReportTitle -let dummyReportedPostTitle = 'Hacker, Freaks und Funktionäre' -let dummyReportedPostSlug = 'hacker-freaks-und-funktionareder-ccc' -let dummyAuthorName = 'Jenny Rostock' +let davidIrvingPostTitle = 'The Truth about the Holocaust' +let davidIrvingPostSlug = 'the-truth-about-the-holocaust' +let davidIrvingName = 'David Irving' const savePostTitle = $post => { return $post @@ -23,21 +23,27 @@ Given("I see David Irving's post on the landing page", page => { }) Given("I see David Irving's post on the post page", page => { - cy.visit(`/post/${dummyReportedPostSlug}`) - cy.contains(dummyReportedPostTitle) // wait + cy.visit(`/post/${davidIrvingPostSlug}`) + cy.contains(davidIrvingPostTitle) // wait }) Given('I am logged in with a {string} role', role => { - cy.loginAs(role) + cy.factory().create('User', { + email: `${role}@example.org`, + password: '1234', + role + }) + cy.login({ + email: `${role}@example.org`, + password: '1234' + }) }) When( 'I click on "Report Contribution" from the triple dot menu of the post', () => { - //TODO: match the created post title, not a dummy post title - cy.contains('.ds-card', dummyReportedPostTitle) + cy.contains('.ds-card', davidIrvingPostTitle) .find('.content-menu-trigger') - .first() .click() cy.get('.popover .ds-menu-item-link') @@ -49,8 +55,7 @@ When( When( 'I click on "Report User" from the triple dot menu in the user info box', () => { - //TODO: match the created post author, not a dummy author - cy.contains('.ds-card', dummyAuthorName) + cy.contains('.ds-card', davidIrvingName) .find('.content-menu-trigger') .first() .click() @@ -106,12 +111,8 @@ Then(`I can't see the moderation menu item`, () => { .should('not.exist') }) -When(/^I confirm the reporting dialog .*:$/, () => { - //TODO: take message from method argument - //TODO: match the right post - const message = 'Do you really want to report the' +When(/^I confirm the reporting dialog .*:$/, (message) => { cy.contains(message) // wait for element to become visible - //TODO: cy.get('.ds-modal').contains(dummyReportedPostTitle) cy.get('.ds-modal').within(() => { cy.get('button') .contains('Send Report') @@ -120,22 +121,28 @@ When(/^I confirm the reporting dialog .*:$/, () => { }) Given('somebody reported the following posts:', table => { - table.hashes().forEach(row => { - //TODO: calll factory here - // const options = Object.assign({}, row, { reported: true }) - //create('post', options) + table.hashes().forEach(({ id }) => { + const reporter = { + email: `reporter${id}@example.org`, + password: '1234' + } + cy.factory() + .create('User', reporter) + .authenticateAs(reporter) + .create('Report', { + description: "I don't like this post", + resource: { id, type: 'contribution' } + }) }) }) Then('I see all the reported posts including the one from above', () => { - //TODO: match the right post cy.get('table tbody').within(() => { - cy.contains('tr', dummyReportedPostTitle) + cy.contains('tr', davidIrvingPostTitle) }) }) Then('each list item links to the post page', () => { - //TODO: match the right post - cy.contains(dummyReportedPostTitle).click() + cy.contains(davidIrvingPostTitle).click() cy.location('pathname').should('contain', '/post') }) diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js index b7b283e2b..3aa6022a8 100644 --- a/cypress/integration/common/settings.js +++ b/cypress/integration/common/settings.js @@ -4,7 +4,6 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps' let aboutMeText let myLocation -let myName const matchNameInUserMenu = name => { cy.get('.avatar-menu').click() // open @@ -12,18 +11,13 @@ const matchNameInUserMenu = name => { cy.get('.avatar-menu').click() // close again } -const setUserName = name => { +When('I save {string} as my new name', name => { cy.get('input[id=name]') .clear() .type(name) cy.get('[type=submit]') .click() .not('[disabled]') - myName = name -} - -When('I save {string} as my new name', name => { - setUserName(name) }) When('I save {string} as my location', location => { @@ -47,31 +41,20 @@ When('I have the following self-description:', text => { aboutMeText = text }) -When('my username is {string}', name => { - if (myName !== name) { - setUserName(name) - } - matchNameInUserMenu(name) -}) - When('people visit my profile page', url => { - cy.visitMyProfile() + cy.openPage('/profile/peter-pan') }) When('they can see the text in the info box below my avatar', () => { cy.contains(aboutMeText) }) -When('I changed my username to {string} previously', name => { - myName = name -}) - Then('they can see the location in the info box below my avatar', () => { - matchNameInUserMenu(myName) + cy.contains(myLocation) }) -Then('my new username is still there', () => { - matchNameInUserMenu(myName) +Then('the name {string} is still there', name => { + matchNameInUserMenu(name) }) Then( diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index cd4280578..c17c2729b 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -1,26 +1,87 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' import { getLangByName } from '../../support/helpers' -import users from '../../fixtures/users.json' /* global cy */ +let lastPost = {} + +const loginCredentials = { + email: 'peterpan@example.org', + password: '1234' +} +const narratorParams = { + name: 'Peter Pan', + ...loginCredentials +} + Given('I am logged in', () => { - cy.loginAs('admin') -}) -Given('I am logged in as {string}', userType => { - cy.loginAs(userType) + cy.login(loginCredentials) }) Given('we have a selection of tags and categories as well as posts', () => { - // TODO: use db factories instead of seed data + cy.factory() + .authenticateAs(loginCredentials) + .create('Category', { + id: 'cat1', + name: 'Just For Fun', + slug: 'justforfun', + icon: 'smile' + }) + .create('Category', { + id: 'cat2', + name: 'Happyness & Values', + slug: 'happyness-values', + icon: 'heart-o' + }) + .create('Category', { + id: 'cat3', + name: 'Health & Wellbeing', + slug: 'health-wellbeing', + icon: 'medkit' + }) + .create('Tag', { id: 't1', name: 'Ecology' }) + .create('Tag', { id: 't2', name: 'Nature' }) + .create('Tag', { id: 't3', name: 'Democracy' }) + + const someAuthor = { + id: 'authorId', + email: 'author@example.org', + password: '1234' + } + cy.factory() + .create('User', someAuthor) + .authenticateAs(someAuthor) + .create('Post', { id: 'p0' }) + .create('Post', { id: 'p1' }) + cy.factory() + .authenticateAs(loginCredentials) + .create('Post', { id: 'p2' }) + .relate('Post', 'Categories', { from: 'p0', to: 'cat1' }) + .relate('Post', 'Categories', { from: 'p1', to: 'cat2' }) + .relate('Post', 'Categories', { from: 'p2', to: 'cat1' }) + .relate('Post', 'Tags', { from: 'p0', to: 't1' }) + .relate('Post', 'Tags', { from: 'p0', to: 't2' }) + .relate('Post', 'Tags', { from: 'p0', to: 't3' }) + .relate('Post', 'Tags', { from: 'p1', to: 't2' }) + .relate('Post', 'Tags', { from: 'p1', to: 't3' }) + .relate('Post', 'Tags', { from: 'p2', to: 't3' }) }) -Given('my account has the following details:', table => { - // TODO: use db factories instead of seed data +Given('we have the following user accounts:', table => { + table.hashes().forEach(params => { + cy.factory().create('User', params) + }) +}) + +Given('I have a user account', () => { + cy.factory().create('User', narratorParams) }) Given('my user account has the role {string}', role => { - // TODO: use db factories instead of seed data + cy.factory().create('User', { + role, + ...loginCredentials + }) }) When('I log out', cy.logout) @@ -34,10 +95,10 @@ Given('I am on the {string} page', page => { }) When('I fill in my email and password combination and click submit', () => { - cy.login('admin@example.org', 1234) + cy.login(loginCredentials) }) -When('I refresh the page', () => { +When(/(?:when )?I refresh the page/, () => { cy.reload() }) @@ -49,7 +110,7 @@ When('I log out through the menu in the top right corner', () => { }) Then('I can see my name {string} in the dropdown menu', () => { - cy.get('.avatar-menu-popover').should('contain', users.admin.name) + cy.get('.avatar-menu-popover').should('contain', narratorParams.name) }) Then('I see the login screen again', () => { @@ -63,7 +124,7 @@ Then('I can click on my profile picture in the top right corner', () => { Then('I am still logged in', () => { cy.get('.avatar-menu').click() - cy.get('.avatar-menu-popover').contains(users.admin.name) + cy.get('.avatar-menu-popover').contains(narratorParams.name) }) When('I select {string} in the language menu', name => { @@ -90,9 +151,18 @@ When('I press {string}', label => { }) Given('we have the following posts in our database:', table => { - table.hashes().forEach(row => { - //TODO: calll factory here - //create('post', row) + table.hashes().forEach(({ Author, id, title, content }) => { + cy.factory() + .create('User', { + name: Author, + email: `${Author}@example.org`, + password: '1234' + }) + .authenticateAs({ + email: `${Author}@example.org`, + password: '1234' + }) + .create('Post', { id, title, content }) }) }) @@ -103,3 +173,42 @@ Then('I see a success message:', message => { When('I click on the avatar menu in the top right corner', () => { cy.get('.avatar-menu').click() }) + +When( + 'I click on the big plus icon in the bottom right corner to create post', + () => { + cy.get('.post-add-button').click() + } +) + +Given('I previously created a post', () => { + cy.factory() + .authenticateAs(loginCredentials) + .create('Post', lastPost) +}) + +When('I choose {string} as the title of the post', title => { + lastPost.title = title.replace('\n', ' ') + cy.get('input[name="title"]').type(lastPost.title) +}) + +When('I type in the following text:', text => { + lastPost.content = text.replace('\n', ' ') + cy.get('.ProseMirror').type(lastPost.content) +}) + +Then('the post shows up on the landing page at position {int}', index => { + cy.openPage('landing') + const selector = `:nth-child(${index}) > .ds-card > .ds-card-content` + cy.get(selector).should('contain', lastPost.title) + cy.get(selector).should('contain', lastPost.content) +}) + +Then('I get redirected to {string}', route => { + cy.location('pathname').should('contain', route) +}) + +Then('the post was saved successfully', () => { + cy.get('.ds-card-header > .ds-heading').should('contain', lastPost.title) + cy.get('.content').should('contain', lastPost.content) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ebbd6acd1..5b4c2055b 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -35,14 +35,7 @@ Cypress.Commands.add('switchLanguage', (name, force) => { } }) -Cypress.Commands.add('visitMyProfile', () => { - cy.get('.avatar-menu').click() - cy.get('.avatar-menu-popover') - .find('a[href^="/profile/"]') - .click() -}) - -Cypress.Commands.add('login', (email, password) => { +Cypress.Commands.add('login', ({ email, password }) => { cy.visit(`/login`) cy.get('input[name=email]') .trigger('focus') @@ -56,11 +49,6 @@ Cypress.Commands.add('login', (email, password) => { cy.location('pathname').should('eq', '/') // we're in! }) -Cypress.Commands.add('loginAs', role => { - role = role || 'admin' - cy.login(users[role].email, users[role].password) -}) - Cypress.Commands.add('logout', (email, password) => { cy.visit(`/logout`) cy.location('pathname').should('contain', '/login') // we're out diff --git a/cypress/support/factories.js b/cypress/support/factories.js new file mode 100644 index 000000000..95355f414 --- /dev/null +++ b/cypress/support/factories.js @@ -0,0 +1,43 @@ +// TODO: find a better way how to import the factories +import Factory from '../../../Nitro-Backend/src/seed/factories' +import { getDriver } from '../../../Nitro-Backend/src/bootstrap/neo4j' + +const neo4jDriver = getDriver({ + uri: Cypress.env('NEO4J_URI'), + username: Cypress.env('NEO4J_USERNAME'), + password: Cypress.env('NEO4J_PASSWORD') +}) +const factory = Factory({ neo4jDriver }) +const seedServerHost = Cypress.env('SEED_SERVER_HOST') + +beforeEach(async () => { + await factory.cleanDatabase({ seedServerHost, neo4jDriver }) +}) + +Cypress.Commands.add('factory', () => { + return Factory({seedServerHost}) +}) + +Cypress.Commands.add( + 'create', + { prevSubject: true }, + (factory, node, properties) => { + return factory.create(node, properties) + } +) + +Cypress.Commands.add( + 'relate', + { prevSubject: true }, + (factory, node, relationship, properties) => { + return factory.relate(node, relationship, properties) + } +) + +Cypress.Commands.add( + 'authenticateAs', + { prevSubject: true }, + (factory, loginCredentials) => { + return factory.authenticateAs(loginCredentials) + } +) diff --git a/cypress/support/index.js b/cypress/support/index.js index d68db96df..3519487bf 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -15,6 +15,7 @@ // Import commands.js using ES2015 syntax: import './commands' +import './factories' // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/docker-compose.override.yml b/docker-compose.override.yml index e740d8180..6edc22f25 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -8,7 +8,9 @@ services: volumes: - .:/nitro-web - node_modules:/nitro-web/node_modules + - nuxt:/nitro-web/.nuxt command: yarn run dev volumes: node_modules: + nuxt: diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index ced9719a2..8bd536a7b 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -5,3 +5,5 @@ services: build: context: . target: build-and-test + environment: + - BACKEND_URL=http://backend:4123 diff --git a/graphql/PostMutations.js b/graphql/PostMutations.js new file mode 100644 index 000000000..d92445744 --- /dev/null +++ b/graphql/PostMutations.js @@ -0,0 +1,28 @@ +import gql from 'graphql-tag' + +export default app => { + return { + CreatePost: gql(` + mutation($title: String!, $content: String!) { + CreatePost(title: $title, content: $content) { + id + title + slug + content + contentExcerpt + } + } + `), + UpdatePost: gql(` + mutation($id: ID!, $title: String!, $content: String!) { + UpdatePost(id: $id, title: $title, content: $content) { + id + title + slug + content + contentExcerpt + } + } + `) + } +} diff --git a/locales/de.json b/locales/de.json index b5d5be9c5..b3e6289ac 100644 --- a/locales/de.json +++ b/locales/de.json @@ -5,7 +5,8 @@ "create": "Erstellen", "save": "Speichern", "edit": "Bearbeiten", - "delete": "Löschen" + "delete": "Löschen", + "cancel": "Abbrechen" }, "login": { "copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.", @@ -100,6 +101,14 @@ "reporter": "gemeldet von" } }, + "contribution": { + "edit": "Beitrag bearbeiten", + "delete": "Beitrag löschen" + }, + "comment": { + "edit": "Kommentar bearbeiten", + "delete": "Kommentar löschen" + }, "disable": { "user": { "title": "Nutzer sperren", diff --git a/locales/en.json b/locales/en.json index 023e34835..cfe634675 100644 --- a/locales/en.json +++ b/locales/en.json @@ -5,7 +5,8 @@ "create": "Create", "save": "Save", "edit": "Edit", - "delete": "Delete" + "delete": "Delete", + "cancel": "Cancel" }, "login": { "copy": "If you already have a human-connection account, login here.", @@ -100,6 +101,14 @@ "reporter": "reported by" } }, + "contribution": { + "edit": "Edit Contribution", + "delete": "Delete Contribution" + }, + "comment": { + "edit": "Edit Comment", + "delete": "Delete Comment" + }, "disable": { "user": { "title": "Disable User", diff --git a/package.json b/package.json index 4f7554378..f6508d6ec 100644 --- a/package.json +++ b/package.json @@ -46,18 +46,22 @@ "express": "~4.16.4", "graphql": "~14.1.1", "jsonwebtoken": "~8.5.0", + "linkify-it": "~2.1.0", "nuxt": "~2.4.3", "nuxt-env": "~0.1.0", "portal-vue": "~1.5.1", "@human-connection/styleguide": "~0.5.2", "v-tooltip": "~2.0.0-rc.33", "vue-count-to": "~1.0.13", + "string-hash": "^1.1.3", + "tiptap": "^1.13.0", + "tiptap-extensions": "^1.13.0", "vue-izitoast": "1.1.2", "vue-sweetalert-icons": "~3.2.0", "vuex-i18n": "~1.11.0" }, "devDependencies": { - "@babel/core": "~7.3.3", + "@babel/core": "~7.3.4", "@babel/preset-env": "~7.3.1", "@vue/cli-shared-utils": "~3.4.0", "@vue/eslint-config-prettier": "~4.0.1", diff --git a/pages/index.vue b/pages/index.vue index ec0f3f31e..7bc85b3a4 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -13,6 +13,16 @@ + + + + + diff --git a/pages/post/_slug/index.vue b/pages/post/_slug/index.vue index 200a9f472..68655a31f 100644 --- a/pages/post/_slug/index.vue +++ b/pages/post/_slug/index.vue @@ -12,6 +12,7 @@ context="contribution" :item-id="post.id" :name="post.title" + :is-owner="isAuthor(post.author.id)" /> @@ -97,6 +98,7 @@ style="float-right" :item-id="comment.id" :name="comment.author.name" + :is-owner="isAuthor(comment.author.id)" /> @@ -160,6 +162,11 @@ export default { this.title = this.post.title } }, + methods: { + isAuthor(id) { + return this.$store.getters['auth/user'].id === id + } + }, apollo: { Post: { query() { diff --git a/pages/post/_slug/more-info.vue b/pages/post/_slug/more-info.vue index 05135b891..847975757 100644 --- a/pages/post/_slug/more-info.vue +++ b/pages/post/_slug/more-info.vue @@ -38,7 +38,7 @@ @@ -70,7 +70,7 @@ export default { }, computed: { post() { - return this.Post ? this.Post[0] : {} + return this.Post ? this.Post[0] || {} : {} } }, apollo: { diff --git a/pages/post/create.vue b/pages/post/create.vue new file mode 100644 index 000000000..72cf65174 --- /dev/null +++ b/pages/post/create.vue @@ -0,0 +1,24 @@ + + + diff --git a/pages/post/edit/_id.vue b/pages/post/edit/_id.vue new file mode 100644 index 000000000..156bcfd23 --- /dev/null +++ b/pages/post/edit/_id.vue @@ -0,0 +1,77 @@ + + + diff --git a/pages/profile/_slug.vue b/pages/profile/_slug.vue index ad99402c7..98d3b9f48 100644 --- a/pages/profile/_slug.vue +++ b/pages/profile/_slug.vue @@ -23,6 +23,7 @@ context="user" :item-id="user.id" :name="user.name" + :is-owner="myProfile" /> @@ -251,6 +252,17 @@ + + +