diff --git a/alfa-client/.gitignore b/alfa-client/.gitignore index a69cc6f2555e13e9a76cb9fe95054f68cee336bf..0fd5e011fd8d6b4bf1dde4eced5edf2744d7f6de 100644 --- a/alfa-client/.gitignore +++ b/alfa-client/.gitignore @@ -5,10 +5,10 @@ /tmp /out-tsc junit.xml -/apps/alfa-e2e/reports -/apps/alfa-e2e/reports_einheitlicher-ansprechpartner -/apps/alfa-e2e/recordings -/apps/alfa-e2e/cypress +/apps/*/reports +/apps/*/reports_einheitlicher-ansprechpartner +/apps/*/recordings +/apps/*/cypress .scannerwork test-report.xml /.angular/cache/* diff --git a/alfa-client/apps/admin-e2e/cypress.config.json b/alfa-client/apps/admin-e2e/cypress.config.json new file mode 100644 index 0000000000000000000000000000000000000000..04fd739596f02bde492782acbb249cc7eef61cfd --- /dev/null +++ b/alfa-client/apps/admin-e2e/cypress.config.json @@ -0,0 +1,28 @@ +{ + "baseUrl": "http://localhost:4301", + "env": { + "dbUrl": "mongodb://localhost:27018", + "database": "local", + "keycloakRealm": "by-e2e-tests-local-dev", + "keycloakUrl": "https://sso.dev.by.ozg-cloud.de/", + "keycloakClient": "admin" + }, + "fileServerFolder": ".", + "fixturesFolder": "./src/fixtures", + "video": false, + "videosFolder": "./reports/videos", + "screenshotsFolder": "./reports/screenshots", + "chromeWebSecurity": false, + "reporter": "../../node_modules/cypress-mochawesome-reporter", + "defaultCommandTimeout": 10000, + "specPattern": "src/e2e/**/*.cy.{js,jsx,ts,tsx}", + "supportFile": "src/support/e2e.ts", + "testIsolation": false, + "reporterOptions": { + "html": false, + "json": true, + "reportDir": "./reports/mochawesome-report", + "reportFilename": "report", + "overwrite": true + } +} \ No newline at end of file diff --git a/alfa-client/apps/admin-e2e/cypress.config.ts b/alfa-client/apps/admin-e2e/cypress.config.ts index 293ed2fa6d2ada5588d9d952e8385eecbd7d92dc..1e57f65df3ed4318fbab8833bc800aed1be41d2e 100644 --- a/alfa-client/apps/admin-e2e/cypress.config.ts +++ b/alfa-client/apps/admin-e2e/cypress.config.ts @@ -1,6 +1,24 @@ import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; import { defineConfig } from 'cypress'; +const cypressConfig = require('./cypress.config.json'); +const cypressEvents = require('./src/support/cypress-tasks.ts'); + export default defineConfig({ - e2e: nxE2EPreset(__filename, { cypressDir: 'src' }), + e2e: { + ...nxE2EPreset(__dirname), + ...cypressConfig, + setupNodeEvents(on, config) { + return cypressEvents(on, config); + }, + }, + retries: { + experimentalStrategy: 'detect-flake-and-pass-on-threshold', + experimentalOptions: { + maxRetries: 2, + passesRequired: 1, + }, + openMode: true, + runMode: true, + }, }); diff --git a/alfa-client/apps/admin-e2e/docker-compose.yml b/alfa-client/apps/admin-e2e/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c58ba68bab694dc4658a032c45d97f96e03fdd9a --- /dev/null +++ b/alfa-client/apps/admin-e2e/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3' + +volumes: + mongodb: + +services: + mongodb: + image: mongo:7 + ports: + - 27017:27017 + volumes: + - mongodb:/data/db + healthcheck: + test: ['CMD', 'mongosh', '--eval', 'db.settings.find()'] + interval: 10s + timeout: 5s + retries: 5 + + administration: + image: docker.ozg-sh.de/administration:${ADMINISTRATION_DOCKER_IMAGE:-snapshot-latest} + platform: linux/amd64 + environment: + - SPRING_PROFILES_ACTIVE=${SPRING_PROFILE:-local,remotekc} + - SPRING_DATA_MONGODB_URI=mongodb://mongodb:27017/config-db + ports: + - 8080:8080 + depends_on: + mongodb: + condition: service_healthy diff --git a/alfa-client/apps/admin-e2e/src/components/buildinfo/buildinfo.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/buildinfo/buildinfo.e2e.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bede900428396575e45c322fe1357c7ac9d3a3a --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/buildinfo/buildinfo.e2e.component.ts @@ -0,0 +1,12 @@ +export class BuildInfoE2EComponent { + private readonly locatorVersion: string = 'build-version'; + private readonly locatorBuildTime: string = 'build-time'; + + public getVersion() { + return cy.getTestElement(this.locatorVersion); + } + + public getBuildTime() { + return cy.getTestElement(this.locatorBuildTime); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/user-profile/current-user-profile.component.e2e.ts b/alfa-client/apps/admin-e2e/src/components/user-profile/current-user-profile.component.e2e.ts new file mode 100644 index 0000000000000000000000000000000000000000..e58121348f8c0dde77d55ac974bbe3947fdac8b0 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/user-profile/current-user-profile.component.e2e.ts @@ -0,0 +1,29 @@ +import { UserProfileE2EComponent } from './user-profile.component.e2e'; + +export class CurrentUserProfileE2EComponent { + private readonly locatorUserIconButton: string = 'user-icon-button'; + private readonly locatorLogoutButton: string = 'logout-button'; + + private readonly locatorRoot: string = 'current-user'; + + public getRoot() { + return cy.getTestElement(this.locatorRoot); + } + + public getUserProfile(): UserProfileE2EComponent { + return new UserProfileE2EComponent(this.locatorRoot); + } + + public logout(): void { + this.getUserIconButton().click(); + this.getLogoutButton().click(); + } + + public getUserIconButton() { + return cy.getTestElement(this.locatorUserIconButton); + } + + private getLogoutButton() { + return cy.getTestElement(this.locatorLogoutButton); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile-icon.component.e2e.ts b/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile-icon.component.e2e.ts new file mode 100644 index 0000000000000000000000000000000000000000..39726bb181554e60310450fe1416680d6f423a8b --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile-icon.component.e2e.ts @@ -0,0 +1,35 @@ +export class UserProfileIconE2EComponent { + private readonly locatorAssignedIcon: string = 'user-profile-assigned'; + private readonly locatorUnassignedIcon: string = 'user-profile-unassigned'; + + private readonly locatorErrorNotFoundIcon: string = 'user-profile-user-not-found'; + private readonly locatorErrorServiceUnavailableIcon: string = 'user-profile-service-unavailable'; + + private readonly locatorUserProfileButton: string = 'user-profile-button-container'; + + constructor(private root: string) {} + + public getRoot() { + return cy.getTestElement(this.root); + } + + public getUnassignedIcon() { + return this.getRoot().findTestElementWithClass(this.locatorUnassignedIcon); + } + + public getAssignedIcon() { + return this.getRoot().findTestElementWithClass(this.locatorAssignedIcon); + } + + public getErrorResourceNotFoundIcon() { + return this.getRoot().findTestElementWithClass(this.locatorErrorNotFoundIcon); + } + + public getErrorServiceUnavailableIcon() { + return this.getRoot().findTestElementWithClass(this.locatorErrorServiceUnavailableIcon); + } + + public getButton() { + return this.getRoot().getTestElement(this.locatorUserProfileButton); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile-search.component.e2e.ts b/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile-search.component.e2e.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e41d9b1e7a68291ab2891169dcf3ec7eb204ff3 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile-search.component.e2e.ts @@ -0,0 +1,23 @@ +export class UserProfileSearchE2EComponent { + private readonly locatorInput: string = 'Bearbeiter-autocomplete-input'; + private readonly locatorError: string = 'Bearbeiter-autocomplete-error'; + private readonly locatorOptions: string = 'autocomplete-option'; + + constructor(private root: string) {} + + public getRoot() { + return cy.getTestElement(this.root); + } + + public getInput() { + return cy.getTestElement(this.locatorInput); + } + + public getSearchOption(prefix: string) { + return cy.getTestElement(prefix + '-' + this.locatorOptions); + } + + public getError() { + return cy.getTestElement(this.locatorError); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile.component.e2e.ts b/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile.component.e2e.ts new file mode 100644 index 0000000000000000000000000000000000000000..74d883dd96be0f2267d6eecde3979f05098af427 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/user-profile/user-profile.component.e2e.ts @@ -0,0 +1,24 @@ +import { UserProfileIconE2EComponent } from './user-profile-icon.component.e2e'; +import { UserProfileSearchE2EComponent } from './user-profile-search.component.e2e'; + +export class UserProfileE2EComponent { + private readonly locatorUserProfileName: string = 'user-profile-name'; + + constructor(private root: string) {} + + public getRoot() { + return cy.getTestElement(this.root); + } + + public getIconContainer(): UserProfileIconE2EComponent { + return new UserProfileIconE2EComponent(this.root); + } + + public getSearchContainer(): UserProfileSearchE2EComponent { + return new UserProfileSearchE2EComponent(this.root); + } + + public getName() { + return this.getRoot().findTestElementWithClass(this.locatorUserProfileName); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/user-settings/user-settings.component.e2e.ts b/alfa-client/apps/admin-e2e/src/components/user-settings/user-settings.component.e2e.ts new file mode 100644 index 0000000000000000000000000000000000000000..984a1fc4b8ce48a90193eb466e09b9872c857c22 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/user-settings/user-settings.component.e2e.ts @@ -0,0 +1,40 @@ +import { TOGGLE_ELEMENT } from '../../support/angular.util'; + +export class UserSettingsE2EComponent { + private readonly rootLocator: string = 'user-settings'; + private readonly emailBenachrichtigungLocator: string = 'email-benachrichtigung'; + private readonly darkModeLocator: string = 'dark-mode'; + private readonly buttonLocator: string = 'icon-button'; + + public getRoot() { + return cy.getTestElementWithOid(this.rootLocator); + } + + public getEmailBenachrichtigung(): ToggleE2EComponent { + return new ToggleE2EComponent(this.emailBenachrichtigungLocator); + } + + public getDarkMode(): ToggleE2EComponent { + return new ToggleE2EComponent(this.darkModeLocator); + } + + public getButton() { + return this.getRoot().findTestElementWithClass(this.buttonLocator); + } +} + +export class ToggleE2EComponent { + private readonly rootLocator: string; + + constructor(root: string) { + this.rootLocator = root; + } + + public getRoot() { + return cy.getTestElementWithOid(this.rootLocator); + } + + public getToggle() { + return this.getRoot().findElement(TOGGLE_ELEMENT); + } +} diff --git a/alfa-client/apps/admin-e2e/src/e2e/app.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/app.cy.ts deleted file mode 100644 index 4a9fe8c305fdfba9d5d7afddb9d8ca5c8e7713a8..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin-e2e/src/e2e/app.cy.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getGreeting } from '../support/app.po'; - -describe('admin-e2e', () => { - beforeEach(() => cy.visit('/')); - - it('should display welcome message', () => { - // Custom command example, see `../support/commands.ts` file - cy.login('my-email@something.com', 'myPassword'); - - // Function helper example, see `../support/app.po.ts` file - getGreeting().contains(/Welcome/); - }); -}); diff --git a/alfa-client/apps/admin-e2e/src/e2e/app/buildinfo.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/app/buildinfo.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..51874a457e9cad9f1dba85d94937066c86299568 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/e2e/app/buildinfo.cy.ts @@ -0,0 +1,30 @@ +import { BuildInfoE2EComponent } from 'apps/admin-e2e/src/components/buildinfo/buildinfo.e2e.component'; +import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; +import { exist } from 'apps/admin-e2e/src/support/cypress.util'; +import { loginAsAriane } from 'apps/admin-e2e/src/support/user-util'; + +describe('Buildinfo', () => { + const mainPage: MainPage = new MainPage(); + const buildInfo: BuildInfoE2EComponent = mainPage.getBuildInfo(); + + before(() => { + loginAsAriane(); + }); + + after(() => { + // dropCollections(); + }); + + describe('after login', () => { + it('should show ...', { defaultCommandTimeout: 30000 }, () => { + // waitForSpinnerToDisappear(); + // exist(vorgangList.getRoot()); + }); + }); + + describe('buildinfo', () => { + it('should show version', () => { + exist(buildInfo.getVersion()); + }); + }); +}); diff --git a/alfa-client/apps/admin-e2e/src/fixtures/example.json b/alfa-client/apps/admin-e2e/src/fixtures/example.json deleted file mode 100644 index 02e4254378e9785f013be7cc8d94a8229dcbcbb7..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin-e2e/src/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/alfa-client/apps/admin-e2e/src/fixtures/user/user_ariane.json b/alfa-client/apps/admin-e2e/src/fixtures/user/user_ariane.json new file mode 100644 index 0000000000000000000000000000000000000000..17db507b6d5ae19d0e39fbb1423bfda7bcb3ce07 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/fixtures/user/user_ariane.json @@ -0,0 +1,14 @@ +{ + "name": "ariane", + "password": "Y9nk43yrQ_zzIPpfFU-I", + "firstName": "Ariane", + "lastName": "Administrator", + "fullName": "Ariane Administrator", + "email": "ariane.adminsitrator@ozg-sh.de", + "initials": "AA", + "dataTestId": "Ariane_Administrator", + "clientRoles": [], + "groups": [ + "E2E Tests" + ] +} \ No newline at end of file diff --git a/alfa-client/apps/admin-e2e/src/model/user.ts b/alfa-client/apps/admin-e2e/src/model/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..466cedd5d06fc1e8d20e37608ef855ed9e1d0f19 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/model/user.ts @@ -0,0 +1,11 @@ +export interface UserE2E { + uuid: string; + id: string; + name: string; + password: string; + firstName: string; + lastName: string; + fullName: string; + initials: string; + dataTestId: string; +} diff --git a/alfa-client/apps/admin-e2e/src/page-objects/header.po.ts b/alfa-client/apps/admin-e2e/src/page-objects/header.po.ts new file mode 100644 index 0000000000000000000000000000000000000000..d46260e8238f5b182e8d8c79002301ec633c2bd4 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/page-objects/header.po.ts @@ -0,0 +1,27 @@ +import { CurrentUserProfileE2EComponent } from '../components/user-profile/current-user-profile.component.e2e'; +import { UserSettingsE2EComponent } from '../components/user-settings/user-settings.component.e2e'; + +export class HeaderE2EComponent { + private readonly locatorLogo: string = 'admin-logo'; + private readonly locatorRoot: string = 'header'; + + private readonly userSettings: UserSettingsE2EComponent = new UserSettingsE2EComponent(); + private readonly currentUserProfile: CurrentUserProfileE2EComponent = + new CurrentUserProfileE2EComponent(); + + public getRoot() { + return cy.getTestElement(this.locatorRoot); + } + + public getLogo() { + return cy.getTestElement(this.locatorLogo); + } + + public getUserSettings(): UserSettingsE2EComponent { + return this.userSettings; + } + + public getCurrentUserProfile(): CurrentUserProfileE2EComponent { + return this.currentUserProfile; + } +} diff --git a/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts b/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a32afeb1969570dd1dd3fce5a954aa61797f9ad --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts @@ -0,0 +1,23 @@ +import { BuildInfoE2EComponent } from '../components/buildinfo/buildinfo.e2e.component'; +import { HeaderE2EComponent } from './header.po'; + +export class MainPage { + private readonly buildInfo: BuildInfoE2EComponent = new BuildInfoE2EComponent(); + private readonly header: HeaderE2EComponent = new HeaderE2EComponent(); + + public getBuildInfo(): BuildInfoE2EComponent { + return this.buildInfo; + } + + public getHeader(): HeaderE2EComponent { + return this.header; + } +} + +export function waitForSpinnerToDisappear(): boolean { + return cy.getTestElementWithClass('spinner').should('not.exist'); +} + +export function waitforSpinnerToAppear(): void { + // exist(cy.getTestElementWithClass('spinner')); +} diff --git a/alfa-client/apps/admin-e2e/src/support/angular.util.ts b/alfa-client/apps/admin-e2e/src/support/angular.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a6efc19a4f2a233521c5d4fb4ae664f38567508 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/support/angular.util.ts @@ -0,0 +1,57 @@ +import { containClass, mouseEnter, notContainClass } from './cypress.util'; + +export const TOGGLE_ELEMENT: string = 'mat-slide-toggle'; + +enum AngularClassesE2E { + MAT_CHECKED = 'mat-mdc-slide-toggle-checked', + MAT_BUTTONG_TOGGLE_CHECKED = 'mat-button-toggle-checked', + MAT_FOCUSED = 'mat-focused', + CDK_KEYBOARD_FOCUSED = 'cdk-keyboard-focused', + MAT_BADGE_HIDDEN = 'mat-badge-hidden', +} + +enum AngularElementE2E { + MAT_FORM_FIELD = 'mat-form-field', +} + +export function hasTooltip(element: any, value: string) { + mouseEnter(element); + element.get('mat-tooltip-component').contains(value); + // element.get(`div[title="${value}"]`); +} + +export function isChecked(element: any) { + containClass(element, AngularClassesE2E.MAT_CHECKED); +} + +export function isNotChecked(element: any) { + notContainClass(element, AngularClassesE2E.MAT_CHECKED); +} + +export function isButtonToggleChecked(element: any) { + containClass(element, AngularClassesE2E.MAT_BUTTONG_TOGGLE_CHECKED); +} + +export function isButtonToggleNotChecked(element: any) { + notContainClass(element, AngularClassesE2E.MAT_BUTTONG_TOGGLE_CHECKED); +} + +export function getFormField(element: any) { + return element.find(AngularElementE2E.MAT_FORM_FIELD); +} + +export function isMatFocused(element: any) { + containClass(element, AngularClassesE2E.MAT_FOCUSED); +} + +export function isKeyboardFocused(element: any) { + containClass(element, AngularClassesE2E.CDK_KEYBOARD_FOCUSED); +} + +export function expectIconWithoutBadge(element: any): void { + containClass(element, AngularClassesE2E.MAT_BADGE_HIDDEN); +} + +export function expectIconWithBadge(element: any): void { + notContainClass(element, AngularClassesE2E.MAT_BADGE_HIDDEN); +} diff --git a/alfa-client/apps/admin-e2e/src/support/app.po.ts b/alfa-client/apps/admin-e2e/src/support/app.po.ts deleted file mode 100644 index 32934246969c2ecb827ac05677785933a707a54d..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin-e2e/src/support/app.po.ts +++ /dev/null @@ -1 +0,0 @@ -export const getGreeting = () => cy.get('h1'); diff --git a/alfa-client/apps/admin-e2e/src/support/commands.ts b/alfa-client/apps/admin-e2e/src/support/commands.ts index c421a3c47c1aa0f82f17f545268ec5965e6b5a79..b04b0c52a85d7c634bca9dfc0f7cb962fc3503b8 100644 --- a/alfa-client/apps/admin-e2e/src/support/commands.ts +++ b/alfa-client/apps/admin-e2e/src/support/commands.ts @@ -1,35 +1,140 @@ -/// <reference types="cypress" /> - -// *********************************************** -// This example commands.ts shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** - -// eslint-disable-next-line @typescript-eslint/no-namespace +enum HttpMethod { + POST = 'POST', + GET = 'GET', +} + +const DATA_TEST_ID: string = 'data-test-id'; +const DATA_TEST_CLASS: string = 'data-test-class'; + +const ACCES_TOKEN: string = 'access_token'; +const ID_TOKEN: string = 'id_token'; + +enum Header { + CONTENT_TYPE = 'Content-Type', + AUTHORIZATION = 'Authorization', +} + +const CYPRESS_CONFIG_BASE_URL: unknown = 'baseUrl'; + +enum CypressEnv { + KEYCLOAK_CLIENT = 'keycloakClient', + KEYCLOAK_REALM = 'keycloakRealm', + KEYCLOAK_URL = 'keycloakUrl', + SEARCH = 'search', + SMOCKER = 'smocker', +} + +const CONTENT_TYPE_HEADER_VALUE: string = 'application/x-www-form-urlencoded'; + declare namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Chainable<Subject> { - login(email: string, password: string): void; + getTestElementWithOid(oid: string, ...args); + getTestElement(selector: string, ...args); + getTestElementWithClass(selector: string, ...args); + findTestElementWithClass(selector: string, ...args); + findElement(selector: string); + login(user, password); + logout(); + getUserInfo(); } } -// -- This is a parent command -- -Cypress.Commands.add('login', (email, password) => { - console.log('Custom command example: Login', email, password); +Cypress.Commands.add('getTestElement', (selector, ...args) => { + return cy.get(`[${DATA_TEST_ID}~="${selector}"]`, ...args); +}); + +Cypress.Commands.add('getTestElementWithClass', (selector, ...args) => { + console.log( + 'Achtung: Potentiell nicht eindeutiges Ergebnis, weil eine data-test-class mit cy.get() von der DOM-Root aus gesucht wird.', + ); + return cy.get(`[${DATA_TEST_CLASS}="${selector}"]`, ...args); +}); + +Cypress.Commands.add('getTestElementWithOid', (oid, ...args) => { + return cy.getTestElement(oid, ...args); +}); + +Cypress.Commands.add( + 'findTestElementWithClass', + { prevSubject: true }, + (subject: any, selector) => { + return subject.find(`[${DATA_TEST_CLASS}="${selector}"]`); + }, +); + +Cypress.Commands.add('findElement', { prevSubject: true }, (subject: any, selector: string) => { + return subject.find(selector); +}); + +Cypress.Commands.add('login', (user: string, password: string) => { + cy.session(user, () => { + cy.request(buildLoginRequest(user, password)).then((response) => handleLoginResponse(response)); + }); +}); + +function buildLoginRequest(user: string, password: string): any { + return { + method: HttpMethod.POST, + followRedirect: false, + url: `${getKeycloakBaseRealmUrl()}/token`, + headers: { + [Header.CONTENT_TYPE]: CONTENT_TYPE_HEADER_VALUE, + }, + body: buildLoginRequestBody(user, password), + }; +} + +function buildLoginRequestBody(user: string, password: string): any { + return { + client_id: Cypress.env(CypressEnv.KEYCLOAK_CLIENT), + username: user, + password: password, + grant_type: 'password', + redirect_uri: Cypress.config(CYPRESS_CONFIG_BASE_URL), + response_mode: 'fragment', + response_type: 'code', + scope: 'openid', + }; +} + +function handleLoginResponse(response): void { + const authorization: any = `bearer ${response.body.access_token}`; + cy.visit('', authorization); + + window.sessionStorage.setItem(ACCES_TOKEN, response.body.access_token); + window.sessionStorage.setItem(ID_TOKEN, response.body.id_token); + + cy.setCookie('XSRF-TOKEN', response.body.session_state); +} + +Cypress.Commands.add('getUserInfo', () => { + return cy.request({ + method: HttpMethod.GET, + url: `${getKeycloakBaseRealmUrl()}/userinfo`, + headers: { + [Header.AUTHORIZATION]: `bearer ${window.sessionStorage.getItem(ACCES_TOKEN)}`, + }, + }); +}); + +Cypress.Commands.add('logout', () => { + cy.request({ + method: HttpMethod.GET, + url: `${getKeycloakBaseRealmUrl()}/logout`, + headers: { + [Header.CONTENT_TYPE]: CONTENT_TYPE_HEADER_VALUE, + }, + body: { + refresh_token: window.sessionStorage.getItem(ID_TOKEN), + }, + failOnStatusCode: false, + }).then(() => { + window.sessionStorage.clear(); + + cy.visit(''); + }); }); -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +function getKeycloakBaseRealmUrl(): string { + return `${Cypress.env(CypressEnv.KEYCLOAK_URL)}realms/${Cypress.env(CypressEnv.KEYCLOAK_REALM)}/protocol/openid-connect`; +} diff --git a/alfa-client/apps/admin-e2e/src/support/cypress-helper.ts b/alfa-client/apps/admin-e2e/src/support/cypress-helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..2af30a94b0cf72ec893754d07956d21f4908fbcd --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/support/cypress-helper.ts @@ -0,0 +1,195 @@ +import { Interception, RouteHandler, RouteMatcher } from 'cypress/types/net-stubbing'; +import { GridFsChunkE2E, GridFsFileDataE2E, GridFsFileE2E } from '../model/binary-file'; +import { CommandE2E } from '../model/command'; +import { SmockerMocks } from '../model/smocker'; +import { UsermanagerUserE2E } from '../model/usermanager'; +import { VorgangE2E } from '../model/vorgang'; +import { VorgangAttachedItemE2E } from '../model/vorgang-attached-item'; + +enum CypressTasks { + DROP_COLLECTIONS = 'dropCollections', + DROP_USER_MANAGER_COLLECTIONS = 'dropUserManagerCollections', + INIT_COMMAND_DATA = 'initCommandData', + INIT_GRID_FS_FILE_DATA = 'initGridFsFileData', + INIT_GRID_FS_CHUNK_DATA = 'initGridFsChunkData', + INIT_VORGANG_DATA = 'initVorgangData', + INIT_VORGANG_ATTACHED_ITEM_DATA = 'initVorgangAttachedItemData', + INIT_USERMANAGER_DATA = 'initUsermanagerData', + COUNT_FILES = 'countFiles', + DELETE_FOLDER = 'deleteFolder', + UNZIP_FILE = 'unzipDownloadFile', + GET_DOWNLOAD_FILES = 'getDownloadFiles', +} + +enum MongoCollections { + COMMAND = 'command', + FS_CHUNKS = 'fs.chunks', + FS_FILES = 'fs.files', + VORGANG = 'vorgang', + VORGANG_ATTACHED_ITEM = 'vorgangAttachedItem', + USER = 'User', +} + +const DOWNLOAD_FOLDER: string = 'cypress/downloads'; + +export function login(userJsonPath: string): void { + cy.fixture(userJsonPath).then((user) => { + cy.login(user.name, user.password); + }); +} + +export function visitUrl(url: string): void { + cy.visit(url); +} + +export function initCommandData(data: CommandE2E[]): void { + cy.task(CypressTasks.DROP_COLLECTIONS, [MongoCollections.COMMAND]); + cy.task(CypressTasks.INIT_COMMAND_DATA, { collection: MongoCollections.COMMAND, data }); +} + +export function initGridFsData(data: GridFsFileDataE2E[]): void { + const files: GridFsFileE2E[] = []; + let chunks: GridFsChunkE2E[] = []; + + data.forEach((singleData) => { + files.push(singleData.file); + chunks = chunks.concat(singleData.chunks); + }); + + initGridFsFileData(files); + initGridFsChunkData(chunks); +} + +function initGridFsFileData(data: GridFsFileE2E[]): void { + cy.task(CypressTasks.DROP_COLLECTIONS, [MongoCollections.FS_FILES]); + cy.task(CypressTasks.INIT_GRID_FS_FILE_DATA, { collection: MongoCollections.FS_FILES, data }); +} + +function initGridFsChunkData(data: GridFsChunkE2E[]): void { + cy.task(CypressTasks.DROP_COLLECTIONS, [MongoCollections.FS_CHUNKS]); + cy.task(CypressTasks.INIT_GRID_FS_CHUNK_DATA, { collection: MongoCollections.FS_CHUNKS, data }); +} + +export function initVorgangAttachedItemData(data: VorgangAttachedItemE2E[]): void { + cy.task(CypressTasks.DROP_COLLECTIONS, [MongoCollections.VORGANG_ATTACHED_ITEM]); + cy.task(CypressTasks.INIT_VORGANG_ATTACHED_ITEM_DATA, { + collection: MongoCollections.VORGANG_ATTACHED_ITEM, + data, + }); +} + +export function initVorgangData(data: VorgangE2E[]): void { + cy.task(CypressTasks.DROP_COLLECTIONS, [MongoCollections.VORGANG]); + cy.task(CypressTasks.INIT_VORGANG_DATA, { collection: MongoCollections.VORGANG, data }); +} + +export function initSearchIndexData(vorgaenge: VorgangE2E[]): void { + vorgaenge.forEach((vorgang) => cy.addVorgangToSearchIndex(vorgang)); +} + +export function dropSearchIndex() { + cy.removeAllDocumentsFromSearchIndex(); +} + +export function initUsermanagerData(data: UsermanagerUserE2E[]): void { + // cy.task(CypressTasks.DROP_USER_MANAGER_COLLECTIONS, [MongoCollections.USER]); + cy.task(CypressTasks.INIT_USERMANAGER_DATA, { collection: MongoCollections.USER, data }); +} + +export function dropCollections() { + cy.task(CypressTasks.DROP_COLLECTIONS, [ + MongoCollections.COMMAND, + MongoCollections.VORGANG, + MongoCollections.VORGANG_ATTACHED_ITEM, + MongoCollections.FS_FILES, + MongoCollections.FS_CHUNKS, + ]); + // cy.task(CypressTasks.DROP_USER_MANAGER_COLLECTIONS, [MongoCollections.USER]); +} + +export function countDownloadFiles(): Cypress.Chainable<number> { + return cy.task(CypressTasks.COUNT_FILES, DOWNLOAD_FOLDER); +} + +export function deleteDownloadFolder() { + return cy.task(CypressTasks.DELETE_FOLDER, DOWNLOAD_FOLDER); +} + +export function unzipDownloadFile(file: string) { + return cy.task(CypressTasks.UNZIP_FILE, { folderName: DOWNLOAD_FOLDER, fileName: file }); +} + +export function getDownloadFiles(): Cypress.Chainable<Array<string>> { + return cy.task(CypressTasks.GET_DOWNLOAD_FILES, DOWNLOAD_FOLDER); +} + +export function scrollToWindowBottom(): void { + cy.window().scrollTo('bottom'); +} + +export function intercept(method: string, url: string): Cypress.Chainable<null> { + return cy.intercept(method, url); +} + +export function interceptWithResponse( + method, + url: RouteMatcher, + response: RouteHandler, +): Cypress.Chainable<null> { + return cy.intercept(method, url, response); +} + +export function waitOfInterceptor(interceptor: string): Cypress.Chainable<Interception> { + return cy.wait('@' + interceptor); +} + +export function getTestElement(value: string) { + return cy.getTestElement(value); +} + +export function getElement(value: string) { + return cy.get(value); +} + +export function urlShouldInclude(text: string) { + return cy.url().should('include', text); +} + +//TODO: anders loesen -> bad practice +export function wait(ms: number, reason = ''): void { + cy.wait(ms); + if (reason) { + console.log(`Had to wait ${ms}ms because of: ${reason}`); + } +} +// + +export function reload(): void { + cy.reload(); +} + +export function readFileFromDownloads(fileName: string): Cypress.Chainable<any> { + return cy.readFile(`${DOWNLOAD_FOLDER}/${fileName}`, { timeout: 5000 }); +} + +export function pressTab(): void { + cy.realPress('Tab'); +} + +//Config +export function getBaseUrl(): string { + return Cypress.config().baseUrl; +} + +//Env +export function getCypressEnv(value: string) { + return Cypress.env(value); +} + +export function addSmockerMock(mock: SmockerMocks): void { + cy.addMockToSmocker(mock); +} + +export function resetSmocker(): void { + cy.resetSmocker(); +} diff --git a/alfa-client/apps/admin-e2e/src/support/cypress-tasks.ts b/alfa-client/apps/admin-e2e/src/support/cypress-tasks.ts new file mode 100644 index 0000000000000000000000000000000000000000..45c36f60e72be3f3d674522988b28af680b8aa8b --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/support/cypress-tasks.ts @@ -0,0 +1,50 @@ +const fs = require('fs'); + +module.exports = (on: any, config: any) => { + on('after:spec', (spec: Cypress.Spec, results: CypressCommandLine.RunResult) => { + if (results && results.video && results.stats.failures === 0) { + console.log(`Delete recorded video because spec passed: ${results.video}`); + fs.unlinkSync(results.video); + } + }); + + // Workaround für Angular 13 und Cypress mit Webpack 4, + // Siehe https://github.com/cypress-io/cypress/issues/19066#issuecomment-1012055705 + // Entfernen, sobald Cypress Webpack 5 nutzt - https://github.com/cypress-io/cypress/issues/19555 + // Ursache: Angular linker needed to link partial-ivy code, + // see https://angular.io/guide/creating-libraries#consuming-partial-ivy-code-outside-the-angular-cli + // Fehlerbild: + // - Anwendung läuft im Browser, aber nicht in Cypress. + // - Fehlermeldung in Cypress: The injectable 'SystemDateTimeProvider' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available. + // Lösung: + // - NPM-Paket identifizieren, dass "SystemDateTimeProvider" enthält. + // - NPM-Paket im "test" Attribut unten hinzufügen. + const webpackPreprocessor = require('@cypress/webpack-batteries-included-preprocessor'); + const webpackOptions = webpackPreprocessor.defaultOptions.webpackOptions; + + webpackOptions.module.rules.unshift({ + test: /[/\\](@angular|@ngxp|angular-oauth2-oidc)[/\\].+\.m?js$/, + resolve: { + fullySpecified: false, + }, + use: { + loader: 'babel-loader', + options: { + plugins: ['@angular/compiler-cli/linker/babel'], + compact: false, + cacheDirectory: true, + }, + }, + }); + + on( + 'file:preprocessor', + webpackPreprocessor({ + webpackOptions: webpackOptions, + typescript: require.resolve('typescript'), + }), + ); + + return config; + // Ende - Workaround für Angular 13 und Cypress mit Webpack 4 +}; diff --git a/alfa-client/apps/admin-e2e/src/support/cypress.util.ts b/alfa-client/apps/admin-e2e/src/support/cypress.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..081f34e8cb5b61c51a12c6861b133d671439177e --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/support/cypress.util.ts @@ -0,0 +1,115 @@ +import { wait } from './cypress-helper'; + +//TODO Naming der Methoden geradeziehen + +export function containClass(element: any, cssClass: string): void { + element.should('have.class', cssClass); +} + +export function notContainClass(element: any, cssClass: string): void { + element.should('not.have.class', cssClass); +} + +export function exist(element: any): void { + element.should('exist'); +} + +export function notExist(element: any): void { + element.should('not.exist'); +} + +export function haveText(element: any, text: string): void { + element + .invoke('text') + .then((elementText) => elementText.trim()) + .should('equal', text); +} + +export function haveValue(element: any, value: string): void { + element.should('have.value', value); +} + +export function haveFocus(element: any): void { + element.should('have.focus'); +} + +export function mouseEnter(element: any): void { + element.trigger('mouseenter'); +} + +export function mouseOver(element: any): void { + element.trigger('mouseover'); +} + +export function contains(element: any, containing: string): void { + element.should('exist').contains(containing); +} + +export function notContains(element: any, containing: string): void { + element.contains(containing).should('not.exist'); +} + +export function haveLength(element: any, length: number): void { + element.should('have.length', length); +} + +export function beChecked(element: any): void { + element.should('be.checked'); +} + +export function notBeChecked(element: any): void { + element.should('not.be.checked'); +} + +//TODO: "first()" rausnehmen -> im html eine entprechende data-test-id ansprechen?! | trennen in "get" und "verify" +export function shouldFirstContains(element: any, containing: string) { + element.first().should('exist').contains(containing); +} + +export function shouldHaveAttributeBeGreaterThan( + element: any, + attributeName: string, + value: number, +) { + element.first().should('exist').invoke(attributeName).should('be.gt', value); +} + +export function shouldHaveAttributeBeLowerThan(element: any, attributeName: string, value: number) { + element.first().should('exist').invoke(attributeName).should('be.gt', value); +} +// + +export function shouldHaveAttribute(element: any, name: string, value: string) { + element.should('have.attr', name, value); +} + +export function visible(element: any) { + element.should('be.visible'); +} + +export function notBeVisible(element: any) { + element.should('not.be.visible'); +} + +export function enter(element: any): void { + element.clear().type(CypressKeyboardActions.ENTER); +} + +export function enterWith( + element: Cypress.Chainable<JQuery<HTMLElement>>, + value: string, + delayBeforeEnter: number = 200, +): void { + element.clear().type(value); + wait(delayBeforeEnter); + element.type(CypressKeyboardActions.ENTER); +} + +export function backspaceOn(element: any): void { + element.type(CypressKeyboardActions.BACKSPACE); +} + +enum CypressKeyboardActions { + ENTER = '{enter}', + BACKSPACE = '{backspace}', +} diff --git a/alfa-client/apps/admin-e2e/src/support/e2e.ts b/alfa-client/apps/admin-e2e/src/support/e2e.ts index 1c1a9e772baea367e08b1c7b15e65b3fede3d17f..8fcf97c3b1114c050e12cc79e14102ad3cad924a 100644 --- a/alfa-client/apps/admin-e2e/src/support/e2e.ts +++ b/alfa-client/apps/admin-e2e/src/support/e2e.ts @@ -1,5 +1,5 @@ // *********************************************************** -// This example support/e2e.ts is processed and +// This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and @@ -13,5 +13,32 @@ // https://on.cypress.io/configuration // *********************************************************** -// Import commands.ts using ES2015 syntax: +// Import commands.js using ES2015 syntax: +import 'cypress-mochawesome-reporter/register'; +import 'cypress-real-events'; +import 'cypress-timestamps/support'; import './commands'; + +Cypress.on('command:start', ({ attributes }) => { + if (attributes.type === 'parent') { + Cypress.log({ + name: `${new Date().toISOString()} - ${attributes.name}`, + }); + } +}); + +Cypress.on('after:screenshot', ({ testFailure, takenAt }) => { + if (testFailure) { + console.log(`Error at: ${takenAt}`); + } +}); + +Cypress.on('fail', (err) => { + console.error(err); + err.message = new Date().toISOString() + '\n' + err.message; + throw err; +}); + +Cypress.Keyboard.defaults({ + keystrokeDelay: 30, +}); diff --git a/alfa-client/apps/admin-e2e/src/support/user-util.ts b/alfa-client/apps/admin-e2e/src/support/user-util.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f47fa0da41a00b4563278cdcd753349957a8d4d --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/support/user-util.ts @@ -0,0 +1,23 @@ +import { UserE2E } from '../model/user'; +import { login } from './cypress-helper'; + +enum User { + ARIANE = 'user/user_ariane.json', +} + +export function loginAsAriane(): void { + login(User.ARIANE); +} + +//TODO Cleanup +export function loginByUi(user: UserE2E): void { + cy.visit('') + .get('#kc-login') + .should('exist') + .get('#username') + .type(user.name) + .get('#password') + .type(user.password) + .get('#kc-login') + .click(); +} diff --git a/alfa-client/package.json b/alfa-client/package.json index e1523dd25e7b7786a77adf6d0fe52760d69516fe..4e5f97a057455a87edf3bcb6b95e2251c9fcd2f7 100644 --- a/alfa-client/package.json +++ b/alfa-client/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "start": "nx run alfa:serve --port 4300 --disable-host-check", - "start:admin": "nx run admin:serve --port 4300 --disable-host-check", + "start:admin": "nx run admin:serve --port 4301 --disable-host-check", "start:demo": "nx run demo:serve --port 4500 --disable-host-check", "start:debug": "nx run alfa:serve --port 4300 --disable-host-check --verbose", "start-for-screenreader": "nx run alfa:serve --host 192.168.178.20 --port 4300 --disable-host-check --verbose",