diff --git a/.gitignore b/.gitignore index 0de8272fc..52fe4effc 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ static/uploads cypress/videos cypress/screenshots/ +cypress.env.json diff --git a/.travis.yml b/.travis.yml index f5cb1b4ef..4afa4c742 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ 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 . @@ -31,9 +32,8 @@ install: 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 c6c87c6a3..6e3debd69 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,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/components/ContributionForm.vue b/components/ContributionForm.vue index 8e96c35fe..cf7c28ece 100644 --- a/components/ContributionForm.vue +++ b/components/ContributionForm.vue @@ -29,7 +29,7 @@ ghost @click.prevent="$router.back()" > - {{ $t('actions.cancel') }} + {{ $t('actions.cancel') }} - {{ $t('actions.save') }} + {{ $t('actions.save') }} 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 aedbf62bc..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,6 +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 index e889a4783..0193e44bf 100644 --- a/cypress/integration/06.WritePost.feature +++ b/cypress/integration/06.WritePost.feature @@ -4,8 +4,9 @@ Feature: Create a post To say something to everyone in the community Background: - Given I am logged in - Given I am on the "landing" page + 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 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 812b0d751..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') - .find(':nth-child(2)') .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 fadee974b..c17c2729b 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -1,29 +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 = { +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) @@ -37,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() }) @@ -52,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', () => { @@ -66,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 => { @@ -93,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 }) }) }) @@ -107,36 +174,41 @@ 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() -}) +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', () => { - // TODO: create a post in the database + cy.factory() + .authenticateAs(loginCredentials) + .create('Post', lastPost) }) -When('I choose {string} as the title of the post', (title) => { +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.text = text.replace('\n', ' ') - cy.get('.ProseMirror').type(lastPost.text) +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) => { +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.text) + cy.get(selector).should('contain', lastPost.content) }) -Then('I get redirected to {string}', (route) => { +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.text) + 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.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