mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2632-feature-dockerfile-for-federation
This commit is contained in:
commit
891eef4e9c
28
.github/workflows/test.yml
vendored
28
.github/workflows/test.yml
vendored
@ -274,7 +274,7 @@ jobs:
|
|||||||
run: docker run --rm gradido/admin:test yarn run lint
|
run: docker run --rm gradido/admin:test yarn run lint
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# JOB: STYLELINT ADMIN INTERFACE ##############################################
|
# JOB: STYLELINT ADMIN INTERFACE #############################################
|
||||||
##############################################################################
|
##############################################################################
|
||||||
stylelint_admin:
|
stylelint_admin:
|
||||||
name: Stylelint - Admin Interface
|
name: Stylelint - Admin Interface
|
||||||
@ -577,7 +577,7 @@ jobs:
|
|||||||
end-to-end-tests:
|
end-to-end-tests:
|
||||||
name: End-to-End Tests
|
name: End-to-End Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx]
|
needs: [build_test_mariadb, build_test_database_up, build_test_admin, build_test_frontend, build_test_nginx]
|
||||||
steps:
|
steps:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# CHECKOUT CODE ##########################################################
|
# CHECKOUT CODE ##########################################################
|
||||||
@ -601,13 +601,6 @@ jobs:
|
|||||||
path: /tmp
|
path: /tmp
|
||||||
- name: Load Docker Image (Database Up)
|
- name: Load Docker Image (Database Up)
|
||||||
run: docker load < /tmp/database_up.tar
|
run: docker load < /tmp/database_up.tar
|
||||||
- name: Download Docker Image (Backend)
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: docker-backend-test
|
|
||||||
path: /tmp
|
|
||||||
- name: Load Docker Image (Backend)
|
|
||||||
run: docker load < /tmp/backend.tar
|
|
||||||
- name: Download Docker Image (Frontend)
|
- name: Download Docker Image (Frontend)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@ -640,7 +633,11 @@ jobs:
|
|||||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
||||||
|
|
||||||
- name: Boot up test system | docker-compose backend
|
- name: Boot up test system | docker-compose backend
|
||||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
|
run: |
|
||||||
|
cd backend
|
||||||
|
cp .env.test_e2e .env
|
||||||
|
cd ..
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
|
||||||
|
|
||||||
- name: Sleep for 10 seconds
|
- name: Sleep for 10 seconds
|
||||||
run: sleep 10s
|
run: sleep 10s
|
||||||
@ -657,6 +654,9 @@ jobs:
|
|||||||
- name: Boot up test system | docker-compose frontends
|
- name: Boot up test system | docker-compose frontends
|
||||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
|
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
|
||||||
|
|
||||||
|
- name: Boot up test system | docker-compose mailserver
|
||||||
|
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
|
||||||
|
|
||||||
- name: Sleep for 15 seconds
|
- name: Sleep for 15 seconds
|
||||||
run: sleep 15s
|
run: sleep 15s
|
||||||
|
|
||||||
@ -666,12 +666,12 @@ jobs:
|
|||||||
- name: End-to-end tests | run tests
|
- name: End-to-end tests | run tests
|
||||||
id: e2e-tests
|
id: e2e-tests
|
||||||
run: |
|
run: |
|
||||||
cd e2e-tests/cypress/tests/
|
cd e2e-tests/
|
||||||
yarn
|
yarn
|
||||||
yarn run cypress run --spec cypress/e2e/User.Authentication.feature
|
yarn run cypress run --spec cypress/e2e/User.Authentication.feature,cypress/e2e/User.Authentication.ResetPassword.feature
|
||||||
- name: End-to-end tests | if tests failed, upload screenshots
|
- name: End-to-end tests | if tests failed, upload screenshots
|
||||||
if: steps.e2e-tests.outcome == 'failure'
|
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: cypress-screenshots
|
name: cypress-screenshots
|
||||||
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/tests/cypress/screenshots/
|
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/
|
||||||
|
|||||||
9
backend/.env.test_e2e
Normal file
9
backend/.env.test_e2e
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Server
|
||||||
|
JWT_EXPIRES_IN=1m
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL=true
|
||||||
|
EMAIL_TEST_MODUS=false
|
||||||
|
EMAIL_TLS=false
|
||||||
|
# for testing password reset
|
||||||
|
EMAIL_CODE_REQUEST_TIME=1
|
||||||
26
e2e-tests/.eslintrc.js
Normal file
26
e2e-tests/.eslintrc.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
cypress: true,
|
||||||
|
},
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['cypress', 'prettier', '@typescript-eslint' /*, 'jest' */],
|
||||||
|
extends: [
|
||||||
|
'standard',
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
],
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
'no-console': ['error'],
|
||||||
|
'no-debugger': 'error',
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
htmlWhitespaceSensitivity: 'ignore',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
5
e2e-tests/.gitignore
vendored
Normal file
5
e2e-tests/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
cypress/screenshots/
|
||||||
|
cypress/videos/
|
||||||
|
cucumber-messages.ndjson
|
||||||
|
|
||||||
9
e2e-tests/.prettierrc.js
Normal file
9
e2e-tests/.prettierrc.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
semi: false,
|
||||||
|
printWidth: 100,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: "all",
|
||||||
|
tabWidth: 2,
|
||||||
|
bracketSpacing: true,
|
||||||
|
endOfLine: "auto",
|
||||||
|
};
|
||||||
@ -11,7 +11,7 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
FROM cypress/base:16.14.2-slim
|
FROM cypress/base:16.14.2-slim
|
||||||
|
|
||||||
ARG DOCKER_WORKDIR=/tests/
|
ARG DOCKER_WORKDIR="/tests"
|
||||||
WORKDIR $DOCKER_WORKDIR
|
WORKDIR $DOCKER_WORKDIR
|
||||||
|
|
||||||
# install dependencies
|
# install dependencies
|
||||||
@ -1,7 +1,73 @@
|
|||||||
# Gradido end-to-end tests
|
# Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
|
||||||
|
|
||||||
This is still WIP.
|
A setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
|
||||||
|
The tests are organized in feature files written in Gherkin syntax.
|
||||||
|
|
||||||
For automated end-to-end testing one of the frameworks Cypress or Playwright will be utilized.
|
|
||||||
|
|
||||||
For more details on how to run them, see the subfolders' README instructions.
|
## Features under test
|
||||||
|
|
||||||
|
So far these features are initially tested
|
||||||
|
- [User authentication](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Authentication.feature)
|
||||||
|
- [User profile - change password](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/UserProfile.ChangePassword.feature)
|
||||||
|
- [User registration]((https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature)) (WIP)
|
||||||
|
|
||||||
|
|
||||||
|
## Precondition
|
||||||
|
|
||||||
|
Before running the tests, change to the repo's root directory (gradido).
|
||||||
|
|
||||||
|
### Boot up the system under test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seed the database
|
||||||
|
|
||||||
|
The database has to be seeded upfront to every test run.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# change to the backend directory
|
||||||
|
cd /path/to/gradido/gradido/backend
|
||||||
|
|
||||||
|
# install all dependencies
|
||||||
|
yarn
|
||||||
|
|
||||||
|
# seed the database (everytime before running the tests)
|
||||||
|
yarn seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execute the test
|
||||||
|
|
||||||
|
This setup will be integrated in the Gradido Github Actions to automatically support the CI/CD process.
|
||||||
|
For now the test setup can only be used locally in two modes.
|
||||||
|
|
||||||
|
### Run Cypress directly from the code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# change to the tests directory
|
||||||
|
cd /path/to/gradido/e2e-tests/
|
||||||
|
|
||||||
|
# install all dependencies
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# a) run the tests on command line
|
||||||
|
yarn cypress run
|
||||||
|
|
||||||
|
# b) open the Cypress GUI to run the tests in interactive mode
|
||||||
|
yarn cypress open
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Run Cyprss from a separate Docker container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# change to the cypress directory
|
||||||
|
cd /path/to/gradido/e2e-tests/
|
||||||
|
|
||||||
|
# build a Docker image from the Dockerfile
|
||||||
|
docker build -t gradido_e2e-tests-cypress .
|
||||||
|
|
||||||
|
# run the Docker image and execute the given tests
|
||||||
|
docker run -it --network=host gradido_e2e-tests-cypress yarn cypress-e2e
|
||||||
|
```
|
||||||
|
|||||||
79
e2e-tests/cypress.config.ts
Normal file
79
e2e-tests/cypress.config.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { defineConfig } from 'cypress'
|
||||||
|
import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
import browserify from '@badeball/cypress-cucumber-preprocessor/browserify'
|
||||||
|
|
||||||
|
let resetPasswordLink: string
|
||||||
|
|
||||||
|
async function setupNodeEvents(
|
||||||
|
on: Cypress.PluginEvents,
|
||||||
|
config: Cypress.PluginConfigOptions
|
||||||
|
): Promise<Cypress.PluginConfigOptions> {
|
||||||
|
await addCucumberPreprocessorPlugin(on, config)
|
||||||
|
|
||||||
|
on(
|
||||||
|
'file:preprocessor',
|
||||||
|
browserify(config, {
|
||||||
|
typescript: require.resolve('typescript'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
on('task', {
|
||||||
|
setResetPasswordLink: (val) => {
|
||||||
|
return (resetPasswordLink = val)
|
||||||
|
},
|
||||||
|
getResetPasswordLink: () => {
|
||||||
|
return resetPasswordLink
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
on('after:run', (results) => {
|
||||||
|
if (results) {
|
||||||
|
// results will be undefined in interactive mode
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(results.status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
specPattern: '**/*.feature',
|
||||||
|
excludeSpecPattern: '*.js',
|
||||||
|
experimentalSessionAndOrigin: true,
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
chromeWebSecurity: false,
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
supportFile: 'cypress/support/index.ts',
|
||||||
|
viewportHeight: 720,
|
||||||
|
viewportWidth: 1280,
|
||||||
|
video: false,
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
backendURL: 'http://localhost:4000',
|
||||||
|
mailserverURL: 'http://localhost:1080',
|
||||||
|
loginQuery: `query ($email: String!, $password: String!, $publisherId: Int) {
|
||||||
|
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||||
|
email
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
language
|
||||||
|
klickTipp {
|
||||||
|
newsletterState
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
hasElopage
|
||||||
|
publisherId
|
||||||
|
isAdmin
|
||||||
|
creation
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
setupNodeEvents,
|
||||||
|
},
|
||||||
|
})
|
||||||
4
e2e-tests/cypress/.gitignore
vendored
4
e2e-tests/cypress/.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
tests/node_modules/
|
|
||||||
tests/cypress/screenshots/
|
|
||||||
tests/cypress/videos/
|
|
||||||
tests/cucumber-messages.ndjson
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
# Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
|
|
||||||
|
|
||||||
A setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
|
|
||||||
The tests are organized in feature files written in Gherkin syntax.
|
|
||||||
|
|
||||||
|
|
||||||
## Features under test
|
|
||||||
|
|
||||||
So far these features are initially tested
|
|
||||||
- [User authentication](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Authentication.feature)
|
|
||||||
- [User profile - change password](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/UserProfile.ChangePassword.feature)
|
|
||||||
- [User registration]((https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature)) (WIP)
|
|
||||||
|
|
||||||
|
|
||||||
## Precondition
|
|
||||||
|
|
||||||
Before running the tests, change to the repo's root directory (gradido).
|
|
||||||
|
|
||||||
### Boot up the system under test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
### Seed the database
|
|
||||||
|
|
||||||
The database has to be seeded upfront to every test run.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# change to the backend directory
|
|
||||||
cd /path/to/gradido/gradido/backend
|
|
||||||
|
|
||||||
# install all dependencies
|
|
||||||
yarn
|
|
||||||
|
|
||||||
# seed the database (everytime before running the tests)
|
|
||||||
yarn seed
|
|
||||||
```
|
|
||||||
|
|
||||||
## Execute the test
|
|
||||||
|
|
||||||
This setup will be integrated in the Gradido Github Actions to automatically support the CI/CD process.
|
|
||||||
For now the test setup can only be used locally in two modes.
|
|
||||||
|
|
||||||
### Run Cypress directly from the code
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# change to the tests directory
|
|
||||||
cd /path/to/gradido/e2e-tests/cypress/tests
|
|
||||||
|
|
||||||
# install all dependencies
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# a) run the tests on command line
|
|
||||||
yarn cypress run
|
|
||||||
|
|
||||||
# b) open the Cypress GUI to run the tests in interactive mode
|
|
||||||
yarn cypress open
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Run Cyprss from a separate Docker container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# change to the cypress directory
|
|
||||||
cd /path/to/gradido/e2e-tests/cypress/
|
|
||||||
|
|
||||||
# build a Docker image from the Dockerfile
|
|
||||||
docker build -t gradido_e2e-tests-cypress .
|
|
||||||
|
|
||||||
# run the Docker image and execute the given tests
|
|
||||||
docker run -it --network=host gradido_e2e-tests-cypress yarn cypress-e2e
|
|
||||||
```
|
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
Feature: User Authentication - reset password
|
||||||
|
As a user
|
||||||
|
I want to reset my password from the sign in page
|
||||||
|
|
||||||
|
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
|
||||||
|
# Background:
|
||||||
|
# Given the following "users" are in the database:
|
||||||
|
# | email | password | name |
|
||||||
|
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
|
||||||
|
|
||||||
|
Scenario: Reset password from signin page successfully
|
||||||
|
Given the user navigates to page "/login"
|
||||||
|
And the user navigates to the forgot password page
|
||||||
|
When the user enters the e-mail address "bibi@bloxberg.de"
|
||||||
|
And the user submits the e-mail form
|
||||||
|
Then the user receives an e-mail containing the password reset link
|
||||||
|
When the user opens the password reset link in the browser
|
||||||
|
And the user enters the password "12345Aa_"
|
||||||
|
And the user repeats the password "12345Aa_"
|
||||||
|
And the user submits the password form
|
||||||
|
And the user clicks the sign in button
|
||||||
|
Then the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
|
||||||
|
And the user cannot login
|
||||||
|
But the user submits the credentials "bibi@bloxberg.de" "12345Aa_"
|
||||||
|
And the user is logged in with username "Bibi Bloxberg"
|
||||||
@ -11,7 +11,7 @@ Feature: User authentication
|
|||||||
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
|
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
|
||||||
|
|
||||||
Scenario: Log in successfully
|
Scenario: Log in successfully
|
||||||
Given the browser navigates to page "/login"
|
Given the user navigates to page "/login"
|
||||||
When the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
|
When the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
|
||||||
Then the user is logged in with username "Bibi Bloxberg"
|
Then the user is logged in with username "Bibi Bloxberg"
|
||||||
|
|
||||||
@ -4,7 +4,7 @@ Feature: User registration
|
|||||||
|
|
||||||
@skip
|
@skip
|
||||||
Scenario: Register successfully
|
Scenario: Register successfully
|
||||||
Given the browser navigates to page "/register"
|
Given the user navigates to page "/register"
|
||||||
When the user fills name and email "Regina" "Register" "regina@register.com"
|
When the user fills name and email "Regina" "Register" "regina@register.com"
|
||||||
And the user agrees to the privacy policy
|
And the user agrees to the privacy policy
|
||||||
And the user submits the registration form
|
And the user submits the registration form
|
||||||
@ -12,7 +12,7 @@ Feature: User profile - change password
|
|||||||
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
|
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
|
||||||
|
|
||||||
Scenario: Change password successfully
|
Scenario: Change password successfully
|
||||||
Given the browser navigates to page "/profile"
|
Given the user navigates to page "/profile"
|
||||||
And the user opens the change password menu
|
And the user opens the change password menu
|
||||||
When the user fills the password form with:
|
When the user fills the password form with:
|
||||||
| Old password | Aa12345_ |
|
| Old password | Aa12345_ |
|
||||||
18
e2e-tests/cypress/e2e/models/ForgotPasswordPage.ts
Normal file
18
e2e-tests/cypress/e2e/models/ForgotPasswordPage.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class ForgotPasswordPage {
|
||||||
|
// selectors
|
||||||
|
emailInput = 'input[type=email]'
|
||||||
|
submitBtn = 'button[type=submit]'
|
||||||
|
successComponent = '[data-test="forgot-password-success"]'
|
||||||
|
|
||||||
|
enterEmail(email: string) {
|
||||||
|
cy.get(this.emailInput).clear().type(email)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
submitEmail() {
|
||||||
|
cy.get(this.submitBtn).click()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
35
e2e-tests/cypress/e2e/models/LoginPage.ts
Normal file
35
e2e-tests/cypress/e2e/models/LoginPage.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class LoginPage {
|
||||||
|
// selectors
|
||||||
|
emailInput = 'input[type=email]'
|
||||||
|
passwordInput = 'input[type=password]'
|
||||||
|
forgotPasswordLink = '[data-test="forgot-password-link"]'
|
||||||
|
submitBtn = '[type=submit]'
|
||||||
|
emailHint = '#vee_Email'
|
||||||
|
passwordHint = '#vee_Password'
|
||||||
|
|
||||||
|
goto() {
|
||||||
|
cy.visit('/')
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterEmail(email: string) {
|
||||||
|
cy.get(this.emailInput).clear().type(email)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterPassword(password: string) {
|
||||||
|
cy.get(this.passwordInput).clear().type(password)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLogin() {
|
||||||
|
cy.get(this.submitBtn).click()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
openForgotPasswordPage() {
|
||||||
|
cy.get(this.forgotPasswordLink).click()
|
||||||
|
}
|
||||||
|
}
|
||||||
10
e2e-tests/cypress/e2e/models/OverviewPage.ts
Normal file
10
e2e-tests/cypress/e2e/models/OverviewPage.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class OverviewPage {
|
||||||
|
navbarName = '[data-test="navbar-item-username"]'
|
||||||
|
|
||||||
|
goto() {
|
||||||
|
cy.visit('/overview')
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
35
e2e-tests/cypress/e2e/models/ProfilePage.ts
Normal file
35
e2e-tests/cypress/e2e/models/ProfilePage.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class ProfilePage {
|
||||||
|
// selectors
|
||||||
|
openChangePassword = '[data-test=open-password-change-form]'
|
||||||
|
oldPasswordInput = '#password-input-field'
|
||||||
|
newPasswordInput = '#new-password-input-field'
|
||||||
|
newPasswordRepeatInput = '#repeat-new-password-input-field'
|
||||||
|
submitNewPasswordBtn = '[data-test=submit-new-password-btn]'
|
||||||
|
|
||||||
|
goto() {
|
||||||
|
cy.visit('/profile')
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterOldPassword(password: string) {
|
||||||
|
cy.get(this.oldPasswordInput).clear().type(password)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterNewPassword(password: string) {
|
||||||
|
cy.get(this.newPasswordInput).find('input').clear().type(password)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterRepeatPassword(password: string) {
|
||||||
|
cy.get(this.newPasswordRepeatInput).find('input').clear().type(password)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
submitPasswordForm() {
|
||||||
|
cy.get(this.submitNewPasswordBtn).click()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
42
e2e-tests/cypress/e2e/models/RegistrationPage.ts
Normal file
42
e2e-tests/cypress/e2e/models/RegistrationPage.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class RegistrationPage {
|
||||||
|
// selectors
|
||||||
|
firstnameInput = '#registerFirstname'
|
||||||
|
lastnameInput = '#registerLastname'
|
||||||
|
emailInput = '#Email-input-field'
|
||||||
|
checkbox = '#registerCheckbox'
|
||||||
|
submitBtn = '[type=submit]'
|
||||||
|
|
||||||
|
RegistrationThanxHeadline = '.test-message-headline'
|
||||||
|
RegistrationThanxText = '.test-message-subtitle'
|
||||||
|
|
||||||
|
goto() {
|
||||||
|
cy.visit('/register')
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterFirstname(firstname: string) {
|
||||||
|
cy.get(this.firstnameInput).clear().type(firstname)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterLastname(lastname: string) {
|
||||||
|
cy.get(this.lastnameInput).clear().type(lastname)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
enterEmail(email: string) {
|
||||||
|
cy.get(this.emailInput).clear().type(email)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPrivacyCheckbox() {
|
||||||
|
cy.get(this.checkbox).click({ force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
submitRegistrationPage() {
|
||||||
|
cy.get(this.submitBtn).should('be.enabled')
|
||||||
|
cy.get(this.submitBtn).click()
|
||||||
|
}
|
||||||
|
}
|
||||||
32
e2e-tests/cypress/e2e/models/ResetPasswordPage.ts
Normal file
32
e2e-tests/cypress/e2e/models/ResetPasswordPage.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class ResetPasswordPage {
|
||||||
|
// selectors
|
||||||
|
newPasswordBlock = '#new-password-input-field'
|
||||||
|
newPasswordRepeatBlock = '#repeat-new-password-input-field'
|
||||||
|
resetPasswordBtn = 'button[type=submit]'
|
||||||
|
resetPasswordMessageBlock = '[data-test="reset-password-message"]'
|
||||||
|
signinBtn = '.btn.test-message-button'
|
||||||
|
|
||||||
|
enterNewPassword(password: string) {
|
||||||
|
cy.get(this.newPasswordBlock).find('input[type=password]').type(password)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
repeatNewPassword(password: string) {
|
||||||
|
cy.get(this.newPasswordRepeatBlock)
|
||||||
|
.find('input[type=password]')
|
||||||
|
.type(password)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
submitNewPassword() {
|
||||||
|
cy.get(this.resetPasswordBtn).click()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
openSigninPage() {
|
||||||
|
cy.get(this.signinBtn).click()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
17
e2e-tests/cypress/e2e/models/SideNavMenu.ts
Normal file
17
e2e-tests/cypress/e2e/models/SideNavMenu.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class SideNavMenu {
|
||||||
|
// selectors
|
||||||
|
profileMenu = '[data-test=profile-menu]'
|
||||||
|
logoutMenu = '[data-test=logout-menu]'
|
||||||
|
|
||||||
|
openUserProfile() {
|
||||||
|
cy.get(this.profileMenu).click()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
cy.get(this.logoutMenu).click()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
10
e2e-tests/cypress/e2e/models/Toasts.ts
Normal file
10
e2e-tests/cypress/e2e/models/Toasts.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class Toasts {
|
||||||
|
// selectors
|
||||||
|
toastSlot = '.b-toaster-slot'
|
||||||
|
toastTypeSuccess = '.b-toast-success'
|
||||||
|
toastTypeError = '.b-toast-danger'
|
||||||
|
toastTitle = '.gdd-toaster-title'
|
||||||
|
toastMessage = '.gdd-toaster-body'
|
||||||
|
}
|
||||||
17
e2e-tests/cypress/e2e/models/UserEMailSite.ts
Normal file
17
e2e-tests/cypress/e2e/models/UserEMailSite.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
|
export class UserEMailSite {
|
||||||
|
// selectors
|
||||||
|
emailInbox = '.sidebar-emails-container'
|
||||||
|
emailList = '.email-list'
|
||||||
|
emailMeta = '.email-meta'
|
||||||
|
emailSubject = '.subject'
|
||||||
|
|
||||||
|
openRecentPasswordResetEMail() {
|
||||||
|
cy.get(this.emailList)
|
||||||
|
.find('email-item')
|
||||||
|
.filter(':contains(asswor)')
|
||||||
|
.click()
|
||||||
|
expect(cy.get(this.emailSubject)).to('contain', 'asswor')
|
||||||
|
}
|
||||||
|
}
|
||||||
40
e2e-tests/cypress/support/e2e.ts
Normal file
40
e2e-tests/cypress/support/e2e.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import jwtDecode from 'jwt-decode'
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (email, password) => {
|
||||||
|
cy.clearLocalStorage('vuex')
|
||||||
|
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: Cypress.env('backendURL'),
|
||||||
|
body: {
|
||||||
|
operationName: null,
|
||||||
|
variables: {
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
query: Cypress.env('loginQuery'),
|
||||||
|
},
|
||||||
|
}).then(async (response) => {
|
||||||
|
const tokens = response.headers.token
|
||||||
|
const token = Array.isArray(tokens) ? tokens[0] : tokens
|
||||||
|
let tokenTime
|
||||||
|
|
||||||
|
if (!token) return
|
||||||
|
// to avoid JWT InvalidTokenError, the decoding of the token is wrapped
|
||||||
|
// in a try-catch block (see
|
||||||
|
// https://github.com/auth0/jwt-decode/issues/65#issuecomment-395493807)
|
||||||
|
try {
|
||||||
|
tokenTime = jwtDecode(token).exp
|
||||||
|
} catch (tokenDecodingError) {
|
||||||
|
cy.log('JWT decoding error: ', tokenDecodingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
const vuexToken = {
|
||||||
|
token: token,
|
||||||
|
tokenTime: tokenTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.visit('/')
|
||||||
|
window.localStorage.setItem('vuex', JSON.stringify(vuexToken))
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types='cypress' />
|
||||||
|
|
||||||
import "./e2e";
|
import './e2e'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable<Subject> {
|
interface Chainable<Subject> {
|
||||||
login(email: string, password: string): Chainable<any>;
|
login(email: string, password: string): Chainable<any>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
39
e2e-tests/cypress/support/step_definitions/common_steps.ts
Normal file
39
e2e-tests/cypress/support/step_definitions/common_steps.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
import { OverviewPage } from '../../e2e/models/OverviewPage'
|
||||||
|
import { SideNavMenu } from '../../e2e/models/SideNavMenu'
|
||||||
|
import { Toasts } from '../../e2e/models/Toasts'
|
||||||
|
|
||||||
|
Given('the user navigates to page {string}', (page: string) => {
|
||||||
|
cy.visit(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
// login related
|
||||||
|
|
||||||
|
Given(
|
||||||
|
'the user is logged in as {string} {string}',
|
||||||
|
(email: string, password: string) => {
|
||||||
|
cy.login(email, password)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Then('the user is logged in with username {string}', (username: string) => {
|
||||||
|
const overviewPage = new OverviewPage()
|
||||||
|
cy.url().should('include', '/overview')
|
||||||
|
cy.get(overviewPage.navbarName).should('contain', username)
|
||||||
|
})
|
||||||
|
|
||||||
|
Then('the user cannot login', () => {
|
||||||
|
const toast = new Toasts()
|
||||||
|
cy.get(toast.toastSlot).within(() => {
|
||||||
|
cy.get(toast.toastTypeError)
|
||||||
|
cy.get(toast.toastTitle).should('be.visible')
|
||||||
|
cy.get(toast.toastMessage).should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// logout
|
||||||
|
|
||||||
|
Then('the user logs out', () => {
|
||||||
|
const sideNavMenu = new SideNavMenu()
|
||||||
|
sideNavMenu.logout()
|
||||||
|
})
|
||||||
45
e2e-tests/cypress/support/step_definitions/email_steps.ts
Normal file
45
e2e-tests/cypress/support/step_definitions/email_steps.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Then, When } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
import { ResetPasswordPage } from '../../e2e/models/ResetPasswordPage'
|
||||||
|
import { UserEMailSite } from '../../e2e/models/UserEMailSite'
|
||||||
|
|
||||||
|
const userEMailSite = new UserEMailSite()
|
||||||
|
const resetPasswordPage = new ResetPasswordPage()
|
||||||
|
|
||||||
|
Then('the user receives an e-mail containing the password reset link', () => {
|
||||||
|
cy.origin(
|
||||||
|
Cypress.env('mailserverURL'),
|
||||||
|
{ args: userEMailSite },
|
||||||
|
(userEMailSite) => {
|
||||||
|
const linkPattern = /\/reset-password\/[0-9]+\d/
|
||||||
|
|
||||||
|
cy.visit('/') // navigate to user's e-maile site (on fake mail server)
|
||||||
|
cy.get(userEMailSite.emailInbox).should('be.visible')
|
||||||
|
|
||||||
|
cy.get(userEMailSite.emailList)
|
||||||
|
.find('.email-item')
|
||||||
|
.filter(':contains(asswor)')
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
|
||||||
|
cy.get(userEMailSite.emailMeta)
|
||||||
|
.find(userEMailSite.emailSubject)
|
||||||
|
.contains('asswor')
|
||||||
|
|
||||||
|
cy.get('.email-content')
|
||||||
|
.find('.plain-text')
|
||||||
|
.contains(linkPattern)
|
||||||
|
.invoke('text')
|
||||||
|
.then((text) => {
|
||||||
|
const resetPasswordLink = text.match(linkPattern)[0]
|
||||||
|
cy.task('setResetPasswordLink', resetPasswordLink)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
When('the user opens the password reset link in the browser', () => {
|
||||||
|
cy.task('getResetPasswordLink').then((passwordResetLink) => {
|
||||||
|
cy.visit(passwordResetLink)
|
||||||
|
})
|
||||||
|
cy.get(resetPasswordPage.newPasswordRepeatBlock).should('be.visible')
|
||||||
|
})
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { When, And } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
import { ForgotPasswordPage } from '../../e2e/models/ForgotPasswordPage'
|
||||||
|
import { LoginPage } from '../../e2e/models/LoginPage'
|
||||||
|
import { ResetPasswordPage } from '../../e2e/models/ResetPasswordPage'
|
||||||
|
|
||||||
|
const loginPage = new LoginPage()
|
||||||
|
const forgotPasswordPage = new ForgotPasswordPage()
|
||||||
|
const resetPasswordPage = new ResetPasswordPage()
|
||||||
|
|
||||||
|
// login related
|
||||||
|
|
||||||
|
When('the user submits no credentials', () => {
|
||||||
|
loginPage.submitLogin()
|
||||||
|
})
|
||||||
|
|
||||||
|
When(
|
||||||
|
'the user submits the credentials {string} {string}',
|
||||||
|
(email: string, password: string) => {
|
||||||
|
cy.intercept('POST', '/graphql', (req) => {
|
||||||
|
if (
|
||||||
|
req.body.hasOwnProperty('query') &&
|
||||||
|
req.body.query.includes('mutation')
|
||||||
|
) {
|
||||||
|
req.alias = 'login'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loginPage.enterEmail(email)
|
||||||
|
loginPage.enterPassword(password)
|
||||||
|
loginPage.submitLogin()
|
||||||
|
cy.wait('@login').then((interception) => {
|
||||||
|
expect(interception.response.statusCode).equals(200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// password reset related
|
||||||
|
|
||||||
|
And('the user navigates to the forgot password page', () => {
|
||||||
|
loginPage.openForgotPasswordPage()
|
||||||
|
cy.url().should('include', '/forgot-password')
|
||||||
|
})
|
||||||
|
|
||||||
|
When('the user enters the e-mail address {string}', (email: string) => {
|
||||||
|
forgotPasswordPage.enterEmail(email)
|
||||||
|
})
|
||||||
|
|
||||||
|
And('the user submits the e-mail form', () => {
|
||||||
|
forgotPasswordPage.submitEmail()
|
||||||
|
cy.get(forgotPasswordPage.successComponent).should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
And('the user enters the password {string}', (password: string) => {
|
||||||
|
resetPasswordPage.enterNewPassword(password)
|
||||||
|
})
|
||||||
|
|
||||||
|
And('the user repeats the password {string}', (password: string) => {
|
||||||
|
resetPasswordPage.repeatNewPassword(password)
|
||||||
|
})
|
||||||
|
|
||||||
|
And('the user submits the new password', () => {
|
||||||
|
resetPasswordPage.submitNewPassword()
|
||||||
|
cy.get(resetPasswordPage.resetPasswordMessageBlock).should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
And('the user clicks the sign in button', () => {
|
||||||
|
resetPasswordPage.openSigninPage()
|
||||||
|
cy.url().should('contain', '/login')
|
||||||
|
})
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { And, When } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
import { ProfilePage } from '../../e2e/models/ProfilePage'
|
||||||
|
import { Toasts } from '../../e2e/models/Toasts'
|
||||||
|
|
||||||
|
const profilePage = new ProfilePage()
|
||||||
|
|
||||||
|
And('the user opens the change password menu', () => {
|
||||||
|
cy.get(profilePage.openChangePassword).click()
|
||||||
|
cy.get(profilePage.newPasswordRepeatInput).should('be.visible')
|
||||||
|
cy.get(profilePage.submitNewPasswordBtn).should('be.disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
When('the user fills the password form with:', (table) => {
|
||||||
|
let hashedTableRows = table.rowsHash()
|
||||||
|
profilePage.enterOldPassword(hashedTableRows['Old password'])
|
||||||
|
profilePage.enterNewPassword(hashedTableRows['New password'])
|
||||||
|
profilePage.enterRepeatPassword(hashedTableRows['Repeat new password'])
|
||||||
|
cy.get(profilePage.submitNewPasswordBtn).should('be.enabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
And('the user submits the password form', () => {
|
||||||
|
profilePage.submitPasswordForm()
|
||||||
|
})
|
||||||
|
|
||||||
|
When('the user is presented a {string} message', (type: string) => {
|
||||||
|
const toast = new Toasts()
|
||||||
|
cy.get(toast.toastSlot).within(() => {
|
||||||
|
cy.get(toast.toastTypeSuccess)
|
||||||
|
cy.get(toast.toastTitle).should('be.visible')
|
||||||
|
cy.get(toast.toastMessage).should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { And, When } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
import { RegistrationPage } from '../../e2e/models/RegistrationPage'
|
||||||
|
|
||||||
|
const registrationPage = new RegistrationPage()
|
||||||
|
|
||||||
|
When(
|
||||||
|
'the user fills name and email {string} {string} {string}',
|
||||||
|
(firstname: string, lastname: string, email: string) => {
|
||||||
|
const registrationPage = new RegistrationPage()
|
||||||
|
registrationPage.enterFirstname(firstname)
|
||||||
|
registrationPage.enterLastname(lastname)
|
||||||
|
registrationPage.enterEmail(email)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
And('the user agrees to the privacy policy', () => {
|
||||||
|
registrationPage.checkPrivacyCheckbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
And('the user submits the registration form', () => {
|
||||||
|
registrationPage.submitRegistrationPage()
|
||||||
|
cy.get(registrationPage.RegistrationThanxHeadline).should('be.visible')
|
||||||
|
cy.get(registrationPage.RegistrationThanxText).should('be.visible')
|
||||||
|
})
|
||||||
@ -1,24 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
plugins: ["cypress", "prettier", "@typescript-eslint"],
|
|
||||||
extends: [
|
|
||||||
"standard",
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
"no-console": ["error"],
|
|
||||||
"no-debugger": "error",
|
|
||||||
"prettier/prettier": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
htmlWhitespaceSensitivity: "ignore",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import { defineConfig } from "cypress";
|
|
||||||
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
|
|
||||||
import browserify from "@badeball/cypress-cucumber-preprocessor/browserify";
|
|
||||||
|
|
||||||
async function setupNodeEvents(
|
|
||||||
on: Cypress.PluginEvents,
|
|
||||||
config: Cypress.PluginConfigOptions
|
|
||||||
): Promise<Cypress.PluginConfigOptions> {
|
|
||||||
await addCucumberPreprocessorPlugin(on, config);
|
|
||||||
|
|
||||||
on(
|
|
||||||
"file:preprocessor",
|
|
||||||
browserify(config, {
|
|
||||||
typescript: require.resolve("typescript"),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
on("after:run", (results) => {
|
|
||||||
if (results) {
|
|
||||||
// results will be undefined in interactive mode
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(results.status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
e2e: {
|
|
||||||
specPattern: "**/*.feature",
|
|
||||||
excludeSpecPattern: "*.js",
|
|
||||||
baseUrl: "http://localhost:3000",
|
|
||||||
chromeWebSecurity: false,
|
|
||||||
defaultCommandTimeout: 10000,
|
|
||||||
supportFile: "cypress/support/index.ts",
|
|
||||||
viewportHeight: 720,
|
|
||||||
viewportWidth: 1280,
|
|
||||||
video: false,
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
openMode: 0,
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
backendURL: "http://localhost:4000",
|
|
||||||
loginQuery: `query ($email: String!, $password: String!, $publisherId: Int) {
|
|
||||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
|
||||||
email
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
language
|
|
||||||
klickTipp {
|
|
||||||
newsletterState
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
hasElopage
|
|
||||||
publisherId
|
|
||||||
isAdmin
|
|
||||||
creation
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
setupNodeEvents,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
export class LoginPage {
|
|
||||||
// selectors
|
|
||||||
emailInput = "input[type=email]";
|
|
||||||
passwordInput = "input[type=password]";
|
|
||||||
submitBtn = "[type=submit]";
|
|
||||||
emailHint = "#vee_Email";
|
|
||||||
passwordHint = "#vee_Password";
|
|
||||||
|
|
||||||
goto() {
|
|
||||||
cy.visit("/");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterEmail(email: string) {
|
|
||||||
cy.get(this.emailInput).clear().type(email);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterPassword(password: string) {
|
|
||||||
cy.get(this.passwordInput).clear().type(password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
submitLogin() {
|
|
||||||
cy.get(this.submitBtn).click();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
export class OverviewPage {
|
|
||||||
navbarName = '[data-test="navbar-item-username"]';
|
|
||||||
|
|
||||||
goto() {
|
|
||||||
cy.visit("/overview");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
export class ProfilePage {
|
|
||||||
// selectors
|
|
||||||
openChangePassword = "[data-test=open-password-change-form]";
|
|
||||||
oldPasswordInput = "#password-input-field";
|
|
||||||
newPasswordInput = "#new-password-input-field";
|
|
||||||
newPasswordRepeatInput = "#repeat-new-password-input-field";
|
|
||||||
submitNewPasswordBtn = "[data-test=submit-new-password-btn]";
|
|
||||||
|
|
||||||
goto() {
|
|
||||||
cy.visit("/profile");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterOldPassword(password: string) {
|
|
||||||
cy.get(this.oldPasswordInput).clear().type(password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterNewPassword(password: string) {
|
|
||||||
cy.get(this.newPasswordInput).find("input").clear().type(password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterRepeatPassword(password: string) {
|
|
||||||
cy.get(this.newPasswordRepeatInput).find("input").clear().type(password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
submitPasswordForm() {
|
|
||||||
cy.get(this.submitNewPasswordBtn).click();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
export class RegistrationPage {
|
|
||||||
// selectors
|
|
||||||
firstnameInput = "#registerFirstname";
|
|
||||||
lastnameInput = "#registerLastname";
|
|
||||||
emailInput = "#Email-input-field";
|
|
||||||
checkbox = "#registerCheckbox";
|
|
||||||
submitBtn = "[type=submit]";
|
|
||||||
|
|
||||||
RegistrationThanxHeadline = ".test-message-headline";
|
|
||||||
RegistrationThanxText = ".test-message-subtitle";
|
|
||||||
|
|
||||||
goto() {
|
|
||||||
cy.visit("/register");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterFirstname(firstname: string) {
|
|
||||||
cy.get(this.firstnameInput).clear().type(firstname);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterLastname(lastname: string) {
|
|
||||||
cy.get(this.lastnameInput).clear().type(lastname);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
enterEmail(email: string) {
|
|
||||||
cy.get(this.emailInput).clear().type(email);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkPrivacyCheckbox() {
|
|
||||||
cy.get(this.checkbox).click({ force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
submitRegistrationPage() {
|
|
||||||
cy.get(this.submitBtn).should("be.enabled");
|
|
||||||
cy.get(this.submitBtn).click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
export class SideNavMenu {
|
|
||||||
// selectors
|
|
||||||
profileMenu = "[data-test=profile-menu]";
|
|
||||||
logoutMenu = "[data-test=logout-menu]";
|
|
||||||
|
|
||||||
openUserProfile() {
|
|
||||||
cy.get(this.profileMenu).click();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
cy.get(this.logoutMenu).click();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
export class Toasts {
|
|
||||||
// selectors
|
|
||||||
toastSlot = ".b-toaster-slot";
|
|
||||||
toastTypeSuccess = ".b-toast-success";
|
|
||||||
toastTypeError = ".b-toast-danger";
|
|
||||||
toastTitle = ".gdd-toaster-title";
|
|
||||||
toastMessage = ".gdd-toaster-body";
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import jwtDecode from "jwt-decode";
|
|
||||||
|
|
||||||
Cypress.Commands.add("login", (email, password) => {
|
|
||||||
cy.clearLocalStorage("vuex");
|
|
||||||
|
|
||||||
cy.request({
|
|
||||||
method: "POST",
|
|
||||||
url: Cypress.env("backendURL"),
|
|
||||||
body: {
|
|
||||||
operationName: null,
|
|
||||||
variables: {
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
},
|
|
||||||
query: Cypress.env("loginQuery"),
|
|
||||||
},
|
|
||||||
}).then(async (response) => {
|
|
||||||
const token = response.headers.token;
|
|
||||||
let tokenTime;
|
|
||||||
|
|
||||||
// to avoid JWT InvalidTokenError, the decoding of the token is wrapped
|
|
||||||
// in a try-catch block (see
|
|
||||||
// https://github.com/auth0/jwt-decode/issues/65#issuecomment-395493807)
|
|
||||||
try {
|
|
||||||
tokenTime = jwtDecode(token).exp;
|
|
||||||
} catch (tokenDecodingError) {
|
|
||||||
cy.log("JWT decoding error: ", tokenDecodingError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const vuexToken = {
|
|
||||||
token: token,
|
|
||||||
tokenTime: tokenTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.visit("/");
|
|
||||||
window.localStorage.setItem("vuex", JSON.stringify(vuexToken));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
||||||
import { LoginPage } from "../../e2e/models/LoginPage";
|
|
||||||
import { OverviewPage } from "../../e2e/models/OverviewPage";
|
|
||||||
import { SideNavMenu } from "../../e2e/models/SideNavMenu";
|
|
||||||
import { Toasts } from "../../e2e/models/Toasts";
|
|
||||||
|
|
||||||
Given("the browser navigates to page {string}", (page: string) => {
|
|
||||||
cy.visit(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
// login-related
|
|
||||||
|
|
||||||
Given(
|
|
||||||
"the user is logged in as {string} {string}",
|
|
||||||
(email: string, password: string) => {
|
|
||||||
cy.login(email, password);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Then("the user is logged in with username {string}", (username: string) => {
|
|
||||||
const overviewPage = new OverviewPage();
|
|
||||||
cy.url().should("include", "/overview");
|
|
||||||
cy.get(overviewPage.navbarName).should("contain", username);
|
|
||||||
});
|
|
||||||
|
|
||||||
Then("the user cannot login", () => {
|
|
||||||
const toast = new Toasts();
|
|
||||||
cy.get(toast.toastSlot).within(() => {
|
|
||||||
cy.get(toast.toastTypeError);
|
|
||||||
cy.get(toast.toastTitle).should("be.visible");
|
|
||||||
cy.get(toast.toastMessage).should("be.visible");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
When(
|
|
||||||
"the user submits the credentials {string} {string}",
|
|
||||||
(email: string, password: string) => {
|
|
||||||
const loginPage = new LoginPage();
|
|
||||||
loginPage.enterEmail(email);
|
|
||||||
loginPage.enterPassword(password);
|
|
||||||
loginPage.submitLogin();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// logout
|
|
||||||
|
|
||||||
Then("the user logs out", () => {
|
|
||||||
const sideNavMenu = new SideNavMenu();
|
|
||||||
sideNavMenu.logout();
|
|
||||||
});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { When } from "@badeball/cypress-cucumber-preprocessor";
|
|
||||||
import { LoginPage } from "../../e2e/models/LoginPage";
|
|
||||||
|
|
||||||
When("the user submits no credentials", () => {
|
|
||||||
const loginPage = new LoginPage();
|
|
||||||
loginPage.submitLogin();
|
|
||||||
});
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { And, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
||||||
import { ProfilePage } from "../../e2e/models/ProfilePage";
|
|
||||||
import { Toasts } from "../../e2e/models/Toasts";
|
|
||||||
|
|
||||||
const profilePage = new ProfilePage();
|
|
||||||
|
|
||||||
And("the user opens the change password menu", () => {
|
|
||||||
cy.get(profilePage.openChangePassword).click();
|
|
||||||
cy.get(profilePage.newPasswordRepeatInput).should("be.visible");
|
|
||||||
cy.get(profilePage.submitNewPasswordBtn).should("be.disabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
When("the user fills the password form with:", (table) => {
|
|
||||||
table = table.rowsHash();
|
|
||||||
profilePage.enterOldPassword(table["Old password"]);
|
|
||||||
profilePage.enterNewPassword(table["New password"]);
|
|
||||||
profilePage.enterRepeatPassword(table["Repeat new password"]);
|
|
||||||
cy.get(profilePage.submitNewPasswordBtn).should("be.enabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
And("the user submits the password form", () => {
|
|
||||||
profilePage.submitPasswordForm();
|
|
||||||
});
|
|
||||||
|
|
||||||
When("the user is presented a {string} message", (type: string) => {
|
|
||||||
const toast = new Toasts();
|
|
||||||
cy.get(toast.toastSlot).within(() => {
|
|
||||||
cy.get(toast.toastTypeSuccess);
|
|
||||||
cy.get(toast.toastTitle).should("be.visible");
|
|
||||||
cy.get(toast.toastMessage).should("be.visible");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { And, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
||||||
import { RegistrationPage } from "../../e2e/models/RegistrationPage";
|
|
||||||
|
|
||||||
const registrationPage = new RegistrationPage();
|
|
||||||
|
|
||||||
When(
|
|
||||||
"the user fills name and email {string} {string} {string}",
|
|
||||||
(firstname: string, lastname: string, email: string) => {
|
|
||||||
const registrationPage = new RegistrationPage();
|
|
||||||
registrationPage.enterFirstname(firstname);
|
|
||||||
registrationPage.enterLastname(lastname);
|
|
||||||
registrationPage.enterEmail(email);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
And("the user agrees to the privacy policy", () => {
|
|
||||||
registrationPage.checkPrivacyCheckbox();
|
|
||||||
});
|
|
||||||
|
|
||||||
And("the user submits the registration form", () => {
|
|
||||||
registrationPage.submitRegistrationPage();
|
|
||||||
cy.get(registrationPage.RegistrationThanxHeadline).should("be.visible");
|
|
||||||
cy.get(registrationPage.RegistrationThanxText).should("be.visible");
|
|
||||||
});
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2016",
|
|
||||||
"lib": ["es6", "dom"],
|
|
||||||
"baseUrl": "../node_modules",
|
|
||||||
"types": ["cypress", "node"],
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts"]
|
|
||||||
}
|
|
||||||
16
e2e-tests/tsconfig.json
Normal file
16
e2e-tests/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["es6", "dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"types": ["cypress", "node"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["cypress/*"],
|
||||||
|
"@models/*": ["cypress/e2e/models/*"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- eslint-disable prettier/prettier -->
|
||||||
<template>
|
<template>
|
||||||
<div class="forgot-password">
|
<div class="forgot-password">
|
||||||
<b-container v-if="enterData">
|
<b-container v-if="enterData">
|
||||||
@ -26,6 +27,7 @@
|
|||||||
<message
|
<message
|
||||||
:headline="success ? $t('message.title') : $t('message.errorTitle')"
|
:headline="success ? $t('message.title') : $t('message.errorTitle')"
|
||||||
:subtitle="success ? $t('message.email') : $t('error.email-already-sent')"
|
:subtitle="success ? $t('message.email') : $t('error.email-already-sent')"
|
||||||
|
:data-test="success ? 'forgot-password-success' : 'forgot-password-error'"
|
||||||
:buttonText="$t('login')"
|
:buttonText="$t('login')"
|
||||||
linkTo="/login"
|
linkTo="/login"
|
||||||
/>
|
/>
|
||||||
|
|||||||
2
frontend/src/pages/Login.vue
Executable file → Normal file
2
frontend/src/pages/Login.vue
Executable file → Normal file
@ -24,7 +24,7 @@
|
|||||||
</b-row>
|
</b-row>
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col class="d-flex justify-content-end mb-4 mb-lg-0">
|
<b-col class="d-flex justify-content-end mb-4 mb-lg-0">
|
||||||
<router-link to="/forgot-password">
|
<router-link to="/forgot-password" data-test="forgot-password-link">
|
||||||
{{ $t('settings.password.forgot_pwd') }}
|
{{ $t('settings.password.forgot_pwd') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b-col>
|
</b-col>
|
||||||
|
|||||||
@ -5,7 +5,12 @@
|
|||||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
|
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
|
||||||
<input-password-confirmation v-model="form" />
|
<input-password-confirmation v-model="form" />
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<b-button type="submit" variant="gradido" class="mt-4">
|
<b-button
|
||||||
|
type="submit"
|
||||||
|
variant="gradido"
|
||||||
|
class="mt-4"
|
||||||
|
data-test="submit-new-password-btn"
|
||||||
|
>
|
||||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys-->
|
<!-- eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys-->
|
||||||
{{ $t(displaySetup.button) }}
|
{{ $t(displaySetup.button) }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user