This is an automated email from the ASF dual-hosted git repository. zehnder pushed a commit to branch STREAMPIPES-380 in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git
commit 52ec1ee852b9e11fdf991b6804c9f22b52cd0a1c Author: Philipp Zehnder <[email protected]> AuthorDate: Tue Jun 22 14:35:18 2021 +0200 [STREAMPIPES-380] Initial commit with new e2e test framework --- ui/cypress.json | 9 ++ ui/cypress/README.md | 49 ++++++ ui/cypress/fixtures/fileTest/random.csv | 11 ++ ui/cypress/fixtures/fileTest/test.csv | 4 + .../pipelineElement/fieldRenamer1/expected.csv | 11 ++ .../pipelineElement/fieldRenamer1/input.csv | 11 ++ .../pipelineElement/numericalFilter1/expected.csv | 6 + .../pipelineElement/numericalFilter1/input.csv | 11 ++ ui/cypress/integration/adapter/fileStream.ts | 27 ++++ .../integration/adapter/machineDataSimulator.ts | 14 ++ .../adapter/randomDataSimulatorStream.spec.ts | 15 ++ .../integration/dataexplorer/testDataExplorer.ts | 11 ++ .../fileManagement/testFileManagement.ts | 16 ++ ui/cypress/integration/pipeline/pipelineTest.ts | 32 ++++ .../pipelineElement/fieldRenamerTest1.ts | 21 +++ .../pipelineElement/numericalFilterTest1.ts | 24 +++ ui/cypress/plugins/index.ts | 16 ++ .../support/builder/GenericAdapterBuilder.ts | 59 +++++++ ui/cypress/support/builder/PipelineBuilder.ts | 39 +++++ .../support/builder/PipelineElementBuilder.ts | 32 ++++ .../support/builder/SpecificAdapterBuilder.ts | 43 +++++ ui/cypress/support/general/dataCy.ts | 14 ++ ui/cypress/support/general/login.ts | 22 +++ ui/cypress/support/general/logout.ts | 20 +++ ui/cypress/support/index.ts | 11 ++ ui/cypress/support/model/AdapterInput.ts | 5 + ui/cypress/support/model/GenericAdapterInput.ts | 8 + ui/cypress/support/model/PipelineElementInput.ts | 6 + ui/cypress/support/model/PipelineInput.ts | 10 ++ ui/cypress/support/model/SpecificAdapterInput.ts | 6 + ui/cypress/support/model/UserInput.ts | 5 + ui/cypress/support/utils/AdapterUtils.ts | 179 +++++++++++++++++++++ ui/cypress/support/utils/DataLakeUtils.ts | 34 ++++ ui/cypress/support/utils/FileManagementUtils.ts | 33 ++++ ui/cypress/support/utils/PipelineUtils.ts | 88 ++++++++++ .../support/utils/ProcessingElementTestUtils.ts | 58 +++++++ ui/cypress/support/utils/StaticPropertyUtils.ts | 21 +++ ui/cypress/tsconfig.json | 11 ++ ui/package.json | 6 +- 39 files changed, 997 insertions(+), 1 deletion(-) diff --git a/ui/cypress.json b/ui/cypress.json new file mode 100644 index 0000000..9439c4e --- /dev/null +++ b/ui/cypress.json @@ -0,0 +1,9 @@ +{ + "projectId": "z5n3e9", + "env": { + "TAKE_SCREENSHOT": "false" + }, + "baseUrl": "http://localhost:8082", + "trashAssetsBeforeRuns": false, + "videoCompression": false +} diff --git a/ui/cypress/README.md b/ui/cypress/README.md new file mode 100644 index 0000000..b598d34 --- /dev/null +++ b/ui/cypress/README.md @@ -0,0 +1,49 @@ +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~ + --> + +# End2End tests with cypress +This folder contains a WIP framework for automated E2E tests of StreamPipes. + +## How to run the tests? +1. Set up and install a clean StreamPipes instance. +2. Start cypress: +```bash +npm run test-e2e +``` + +**User**: [email protected] **Password**: test1234 + +>**Note:** This can be changed in **support/general/login.ts** + + +>**Note:** The base URL can be configured in **cypress.json** + +## Design guidlines +* Each test sets up its own test environment (e.g. upload files) +* Once the test is performed all configurations and system changes should be removed (e.g. delete uploaded files) +* There should not be any dependencies between tests + +## Directories +* **fixtures**: + * Files that are required for tests +* **integrations**: + * Contains the actual tests +* **plugins**: + * Cypress plugins can be added here +* **support**: + * Contains code for the StreamPipes test framework (e.g. model, utils functions, ...) diff --git a/ui/cypress/fixtures/fileTest/random.csv b/ui/cypress/fixtures/fileTest/random.csv new file mode 100644 index 0000000..2420ef5 --- /dev/null +++ b/ui/cypress/fixtures/fileTest/random.csv @@ -0,0 +1,11 @@ +timestamp;count;randomnumber;randomtext +1623871499055;122.0;62.0;c +1623871500059;123.0;46.0;a +1623871501064;124.0;41.0;b +1623871502070;125.0;41.0;b +1623871503078;126.0;22.0;b +1623871504082;127.0;56.0;a +1623871505084;128.0;95.0;b +1623871506086;129.0;77.0;a +1623871507091;130.0;85.0;b +1623871508093;131.0;22.0;a diff --git a/ui/cypress/fixtures/fileTest/test.csv b/ui/cypress/fixtures/fileTest/test.csv new file mode 100644 index 0000000..9374177 --- /dev/null +++ b/ui/cypress/fixtures/fileTest/test.csv @@ -0,0 +1,4 @@ +one,two +1,2 +3,5 +4,6 diff --git a/ui/cypress/fixtures/pipelineElement/fieldRenamer1/expected.csv b/ui/cypress/fixtures/pipelineElement/fieldRenamer1/expected.csv new file mode 100644 index 0000000..c977de6 --- /dev/null +++ b/ui/cypress/fixtures/pipelineElement/fieldRenamer1/expected.csv @@ -0,0 +1,11 @@ +time;newname;randomnumber;randomtext;sp_internal_label;timestamp +1623871499055;122.0;62.0;c;;1.623871499055E12 +1623871500059;123.0;46.0;a;;1.623871500059E12 +1623871501064;124.0;41.0;b;;1.623871501064E12 +1623871502070;125.0;41.0;b;;1.62387150207E12 +1623871503078;126.0;22.0;b;;1.623871503078E12 +1623871504082;127.0;56.0;a;;1.623871504082E12 +1623871505084;128.0;95.0;b;;1.623871505084E12 +1623871506086;129.0;77.0;a;;1.623871506086E12 +1623871507091;130.0;85.0;b;;1.623871507091E12 +1623871508093;131.0;22.0;a;;1.623871508093E12 diff --git a/ui/cypress/fixtures/pipelineElement/fieldRenamer1/input.csv b/ui/cypress/fixtures/pipelineElement/fieldRenamer1/input.csv new file mode 100644 index 0000000..2420ef5 --- /dev/null +++ b/ui/cypress/fixtures/pipelineElement/fieldRenamer1/input.csv @@ -0,0 +1,11 @@ +timestamp;count;randomnumber;randomtext +1623871499055;122.0;62.0;c +1623871500059;123.0;46.0;a +1623871501064;124.0;41.0;b +1623871502070;125.0;41.0;b +1623871503078;126.0;22.0;b +1623871504082;127.0;56.0;a +1623871505084;128.0;95.0;b +1623871506086;129.0;77.0;a +1623871507091;130.0;85.0;b +1623871508093;131.0;22.0;a diff --git a/ui/cypress/fixtures/pipelineElement/numericalFilter1/expected.csv b/ui/cypress/fixtures/pipelineElement/numericalFilter1/expected.csv new file mode 100644 index 0000000..4695aee --- /dev/null +++ b/ui/cypress/fixtures/pipelineElement/numericalFilter1/expected.csv @@ -0,0 +1,6 @@ +time;count;randomnumber;randomtext;sp_internal_label;timestamp +1623871499055;122.0;62.0;c;;1.623871499055E12 +1623871504082;127.0;56.0;a;;1.623871504082E12 +1623871505084;128.0;95.0;b;;1.623871505084E12 +1623871506086;129.0;77.0;a;;1.623871506086E12 +1623871507091;130.0;85.0;b;;1.623871507091E12 diff --git a/ui/cypress/fixtures/pipelineElement/numericalFilter1/input.csv b/ui/cypress/fixtures/pipelineElement/numericalFilter1/input.csv new file mode 100644 index 0000000..2420ef5 --- /dev/null +++ b/ui/cypress/fixtures/pipelineElement/numericalFilter1/input.csv @@ -0,0 +1,11 @@ +timestamp;count;randomnumber;randomtext +1623871499055;122.0;62.0;c +1623871500059;123.0;46.0;a +1623871501064;124.0;41.0;b +1623871502070;125.0;41.0;b +1623871503078;126.0;22.0;b +1623871504082;127.0;56.0;a +1623871505084;128.0;95.0;b +1623871506086;129.0;77.0;a +1623871507091;130.0;85.0;b +1623871508093;131.0;22.0;a diff --git a/ui/cypress/integration/adapter/fileStream.ts b/ui/cypress/integration/adapter/fileStream.ts new file mode 100644 index 0000000..fea214f --- /dev/null +++ b/ui/cypress/integration/adapter/fileStream.ts @@ -0,0 +1,27 @@ +import {AdapterUtils} from '../../support/utils/AdapterUtils'; +import {FileManagementUtils} from '../../support/utils/FileManagementUtils'; +import {GenericAdapterBuilder} from '../../support/builder/GenericAdapterBuilder'; + +describe('Test File Stream Adapter', function () { + + it('Login', function () { + cy.login(); + }); + + FileManagementUtils.addFile('fileTest/random.csv'); + + const adapterInput = GenericAdapterBuilder + .create('File_Stream') + .setName('File Stream Adapter Test') + .setTimestampProperty('timestamp') + .addProtocolInput('input', 'speed','1') + .addProtocolInput('checkbox', 'replaceTimestamp','check') + .setFormat('csv') + .addFormatInput('input', 'delimiter',';') + .addFormatInput('checkbox', 'header','check') + .build(); + + AdapterUtils.testGenericStreamAdapter(adapterInput); + + FileManagementUtils.deleteFile(); +}); \ No newline at end of file diff --git a/ui/cypress/integration/adapter/machineDataSimulator.ts b/ui/cypress/integration/adapter/machineDataSimulator.ts new file mode 100644 index 0000000..b04a85d --- /dev/null +++ b/ui/cypress/integration/adapter/machineDataSimulator.ts @@ -0,0 +1,14 @@ +import {AdapterUtils} from '../../support/utils/AdapterUtils'; +import {SpecificAdapterBuilder} from '../../support/builder/SpecificAdapterBuilder'; + +describe('Test Random Data Simulator Stream Adapter', function () { + + const adapterInput = SpecificAdapterBuilder + .create('Machine_Data_Simulator') + .setName('Machine Data Simulator Test') + .addInput('input', 'wait-time-ms','1000') + .build(); + + AdapterUtils.testSpecificStreamAdapter(adapterInput); + +}); \ No newline at end of file diff --git a/ui/cypress/integration/adapter/randomDataSimulatorStream.spec.ts b/ui/cypress/integration/adapter/randomDataSimulatorStream.spec.ts new file mode 100644 index 0000000..72505a2 --- /dev/null +++ b/ui/cypress/integration/adapter/randomDataSimulatorStream.spec.ts @@ -0,0 +1,15 @@ +import {UserInput} from '../../support/model/UserInput'; +import {AdapterUtils} from '../../support/utils/AdapterUtils'; +import {SpecificAdapterInput} from '../../support/model/SpecificAdapterInput'; +import {SpecificAdapterBuilder} from '../../support/builder/SpecificAdapterBuilder'; + +describe('Test Random Data Simulator Stream Adapter', function () { + + const adapterInput = SpecificAdapterBuilder + .create('Random_Data_Simulator_\\(Stream\\)') + .setName('Random Data Simulator Adapter Test') + .addInput('input', 'wait-time-ms','1000') + .build(); + + AdapterUtils.testSpecificStreamAdapter(adapterInput); +}); diff --git a/ui/cypress/integration/dataexplorer/testDataExplorer.ts b/ui/cypress/integration/dataexplorer/testDataExplorer.ts new file mode 100644 index 0000000..bccdc11 --- /dev/null +++ b/ui/cypress/integration/dataexplorer/testDataExplorer.ts @@ -0,0 +1,11 @@ +import {DataLakeUtils} from '../../support/utils/DataLakeUtils'; + +describe('Test File Management', function () { + it('Login', function () { + cy.login(); + + + }); + + +}); \ No newline at end of file diff --git a/ui/cypress/integration/fileManagement/testFileManagement.ts b/ui/cypress/integration/fileManagement/testFileManagement.ts new file mode 100644 index 0000000..9dbff1a --- /dev/null +++ b/ui/cypress/integration/fileManagement/testFileManagement.ts @@ -0,0 +1,16 @@ +import {FileManagementUtils} from '../../support/utils/FileManagementUtils'; + +describe('Test File Management', function () { + it('Login', function () { + cy.login(); + }); + + FileManagementUtils.addFile('fileTest/test.csv') + + FileManagementUtils.deleteFile() + + it('Logout', function () { + cy.logout(); + }); + +}); \ No newline at end of file diff --git a/ui/cypress/integration/pipeline/pipelineTest.ts b/ui/cypress/integration/pipeline/pipelineTest.ts new file mode 100644 index 0000000..eb53c38 --- /dev/null +++ b/ui/cypress/integration/pipeline/pipelineTest.ts @@ -0,0 +1,32 @@ +import {UserInput} from '../../support/model/UserInput'; +import {AdapterUtils} from '../../support/utils/AdapterUtils'; +import {PipelineUtils} from '../../support/utils/PipelineUtils'; +import {PipelineElementBuilder} from '../../support/builder/PipelineElementBuilder'; +import {PipelineBuilder} from '../../support/builder/PipelineBuilder'; + +describe('Test Random Data Simulator Stream Adapter', function () { + + it('Login', function () { + cy.login(); + }); + + const adapterName = 'simulator'; + AdapterUtils.addMachineDataSimulator(adapterName); + + const pipelineInput = PipelineBuilder.create('Pipeline Test') + .addSource(adapterName) + .addProcessingElement( + PipelineElementBuilder.create('field_renamer') + .addInput('drop-down', 'convert-property','timestamp') + .addInput('input', 'field-name','t') + .build()) + .addSink( + PipelineElementBuilder.create('dashboard_sink') + .addInput('input', 'visualization-name','Demo') + .build()) + .build(); + + PipelineUtils.testPipeline(pipelineInput); + + AdapterUtils.deleteAdapter(); +}); \ No newline at end of file diff --git a/ui/cypress/integration/pipelineElement/fieldRenamerTest1.ts b/ui/cypress/integration/pipelineElement/fieldRenamerTest1.ts new file mode 100644 index 0000000..cf81d0d --- /dev/null +++ b/ui/cypress/integration/pipelineElement/fieldRenamerTest1.ts @@ -0,0 +1,21 @@ +import {ProcessingElementTestUtils} from '../../support/utils/ProcessingElementTestUtils'; +import {PipelineElementBuilder} from '../../support/builder/PipelineElementBuilder'; + +describe('Test Field Renamer 1', function () { + + it('Login', function () { + cy.login(); + }); + + // Config + const testName = 'fieldRenamer1' + const inputFile = 'pipelineElement/fieldRenamer1/input.csv'; + const expectedResultFile = 'pipelineElement/fieldRenamer1/expected.csv'; + + const processor = PipelineElementBuilder.create('field_renamer') + .addInput('drop-down', 'convert-property','count') + .addInput('input', 'field-name','newname') + .build(); + + ProcessingElementTestUtils.testElement(testName, inputFile, expectedResultFile, processor); +}); \ No newline at end of file diff --git a/ui/cypress/integration/pipelineElement/numericalFilterTest1.ts b/ui/cypress/integration/pipelineElement/numericalFilterTest1.ts new file mode 100644 index 0000000..e67d40f --- /dev/null +++ b/ui/cypress/integration/pipelineElement/numericalFilterTest1.ts @@ -0,0 +1,24 @@ +import {UserInput} from '../../support/model/UserInput'; +import {PipelineElementInput} from '../../support/model/PipelineElementInput'; +import {ProcessingElementTestUtils} from '../../support/utils/ProcessingElementTestUtils'; +import {PipelineElementBuilder} from '../../support/builder/PipelineElementBuilder'; + +describe('Test Numerical Filter 1', function () { + + it('Login', function () { + cy.login(); + }); + + // Config + const testName = 'numericalFilter1' + const inputFile = 'pipelineElement/numericalFilter1/input.csv'; + const expectedResultFile = 'pipelineElement/numericalFilter1/expected.csv'; + + const processor = PipelineElementBuilder.create('numerical_filter') + .addInput('drop-down', 'number-mapping','randomnumber') + .addInput('radio', 'operation','\\>') + .addInput('input', 'value','50') + .build(); + + ProcessingElementTestUtils.testElement(testName, inputFile, expectedResultFile, processor); +}); \ No newline at end of file diff --git a/ui/cypress/plugins/index.ts b/ui/cypress/plugins/index.ts new file mode 100644 index 0000000..6fbea7f --- /dev/null +++ b/ui/cypress/plugins/index.ts @@ -0,0 +1,16 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; + diff --git a/ui/cypress/support/builder/GenericAdapterBuilder.ts b/ui/cypress/support/builder/GenericAdapterBuilder.ts new file mode 100644 index 0000000..fbac9f8 --- /dev/null +++ b/ui/cypress/support/builder/GenericAdapterBuilder.ts @@ -0,0 +1,59 @@ +import {UserInput} from '../model/UserInput'; +import {SpecificAdapterInput} from '../model/SpecificAdapterInput'; +import {GenericAdapterInput} from '../model/GenericAdapterInput'; + +export class GenericAdapterBuilder { + genericAdapterInput: GenericAdapterInput; + + constructor(type: string) { + this.genericAdapterInput = new GenericAdapterInput(); + this.genericAdapterInput.adapterType = type; + this.genericAdapterInput.protocolConfiguration = []; + this.genericAdapterInput.formatConfiguration = []; + } + + public static create(name: string) { + return new GenericAdapterBuilder(name); + } + + public setName(name: string) { + this.genericAdapterInput.adapterName = name; + return this; + } + + public setTimestampProperty(timestsmpProperty: string) { + this.genericAdapterInput.timestampProperty = timestsmpProperty; + return this; + } + + public addProtocolInput(type: string, selector: string, value: string) { + const userInput = new UserInput(); + userInput.type = type; + userInput.selector = selector; + userInput.value = value; + + this.genericAdapterInput.protocolConfiguration.push(userInput); + + return this; + } + + public setFormat(format: string) { + this.genericAdapterInput.format = format; + return this; + } + + public addFormatInput(type: string, selector: string, value: string) { + const userInput = new UserInput(); + userInput.type = type; + userInput.selector = selector; + userInput.value = value; + + this.genericAdapterInput.formatConfiguration.push(userInput); + + return this; + } + + build() { + return this.genericAdapterInput; + } +} \ No newline at end of file diff --git a/ui/cypress/support/builder/PipelineBuilder.ts b/ui/cypress/support/builder/PipelineBuilder.ts new file mode 100644 index 0000000..9bba754 --- /dev/null +++ b/ui/cypress/support/builder/PipelineBuilder.ts @@ -0,0 +1,39 @@ +import {UserInput} from '../model/UserInput'; +import {PipelineInput} from '../model/PipelineInput'; +import {PipelineElementInput} from '../model/PipelineElementInput'; + +export class PipelineBuilder { + pipeline: PipelineInput; + + constructor(name: string) { + this.pipeline = new PipelineInput(); + this.pipeline.pipelineName = name; + } + + public static create(name: string) { + return new PipelineBuilder(name); + } + + public addSource(source: string) { + this.pipeline.dataSource = source; + + return this; + } + + public addProcessingElement(processingElement: PipelineElementInput) { + this.pipeline.processingElement = processingElement; + + return this; + } + + public addSink(sink: PipelineElementInput) { + this.pipeline.dataSink = sink; + + return this; + } + + build() { + return this.pipeline; + } + +} diff --git a/ui/cypress/support/builder/PipelineElementBuilder.ts b/ui/cypress/support/builder/PipelineElementBuilder.ts new file mode 100644 index 0000000..805e2db --- /dev/null +++ b/ui/cypress/support/builder/PipelineElementBuilder.ts @@ -0,0 +1,32 @@ +import {UserInput} from '../model/UserInput'; +import {PipelineElementInput} from '../model/PipelineElementInput'; + +export class PipelineElementBuilder { + + pipelineElementInput: PipelineElementInput; + + constructor(name: string) { + this.pipelineElementInput = new PipelineElementInput(); + this.pipelineElementInput.name = name; + this.pipelineElementInput.config = []; + } + + public static create(name: string) { + return new PipelineElementBuilder(name); + } + + public addInput(type: string, selector: string, value: string) { + const userInput = new UserInput(); + userInput.type = type; + userInput.selector = selector; + userInput.value = value; + + this.pipelineElementInput.config.push(userInput); + + return this; + } + + build() { + return this.pipelineElementInput; + } +} diff --git a/ui/cypress/support/builder/SpecificAdapterBuilder.ts b/ui/cypress/support/builder/SpecificAdapterBuilder.ts new file mode 100644 index 0000000..3ce498e --- /dev/null +++ b/ui/cypress/support/builder/SpecificAdapterBuilder.ts @@ -0,0 +1,43 @@ +import {UserInput} from '../model/UserInput'; +import {SpecificAdapterInput} from '../model/SpecificAdapterInput'; + +export class SpecificAdapterBuilder { + + + specificAdapterInput: SpecificAdapterInput; + + constructor(type: string) { + this.specificAdapterInput = new SpecificAdapterInput(); + this.specificAdapterInput.adapterType = type; + this.specificAdapterInput.adapterConfiguration = []; + } + + public static create(name: string) { + return new SpecificAdapterBuilder(name); + } + + public setName(name: string) { + this.specificAdapterInput.adapterName = name; + return this; + } + + public setTimestampProperty(timestsmpProperty: string) { + this.specificAdapterInput.timestampProperty = timestsmpProperty; + return this; + } + + public addInput(type: string, selector: string, value: string) { + const userInput = new UserInput(); + userInput.type = type; + userInput.selector = selector; + userInput.value = value; + + this.specificAdapterInput.adapterConfiguration.push(userInput); + + return this; + } + + build() { + return this.specificAdapterInput; + } +} \ No newline at end of file diff --git a/ui/cypress/support/general/dataCy.ts b/ui/cypress/support/general/dataCy.ts new file mode 100644 index 0000000..4926360 --- /dev/null +++ b/ui/cypress/support/general/dataCy.ts @@ -0,0 +1,14 @@ +declare global { + namespace Cypress { + interface Chainable { + /** + * Select cypress id's ([data-cy=...]) + */ + dataCy(value: string): Chainable<Element> + } + } +} + +export const dataCy = (value) => { + return cy.get(`[data-cy=${value}]`) +}; diff --git a/ui/cypress/support/general/login.ts b/ui/cypress/support/general/login.ts new file mode 100644 index 0000000..a3bcfb3 --- /dev/null +++ b/ui/cypress/support/general/login.ts @@ -0,0 +1,22 @@ +declare global { + namespace Cypress { + interface Chainable { + /** + * Login into streampipes with standard test user + * @example cy.login(); + */ + login: typeof login; + } + } +} + +export const login = () => { + Cypress.Cookies.defaults({ + preserve: "JSESSIONID" + }); + + cy.request('POST', '/streampipes-backend/api/v2/admin/login', { + username: '[email protected]', + password: 'test1234' + }); +}; diff --git a/ui/cypress/support/general/logout.ts b/ui/cypress/support/general/logout.ts new file mode 100644 index 0000000..a195a5b --- /dev/null +++ b/ui/cypress/support/general/logout.ts @@ -0,0 +1,20 @@ +declare global { + namespace Cypress { + interface Chainable { + /** + * Logout of StreamPipes + * @example cy.logout(); + */ + logout: typeof logout; + } + } +} + +export const logout = () => { + Cypress.Cookies.defaults({ + preserve: "JSESSIONID" + }); + + cy.dataCy('sp-user-preferences').click(); + cy.dataCy('sp-logout').click(); +}; \ No newline at end of file diff --git a/ui/cypress/support/index.ts b/ui/cypress/support/index.ts new file mode 100644 index 0000000..1f4d353 --- /dev/null +++ b/ui/cypress/support/index.ts @@ -0,0 +1,11 @@ +import {login} from './general/login'; +import {logout} from './general/logout'; +import {dataCy} from './general/dataCy'; +import 'cypress-file-upload'; + +// General commands +Cypress.Commands.add('login', login); +Cypress.Commands.add('logout', logout); +Cypress.Commands.add('dataCy', dataCy); + + diff --git a/ui/cypress/support/model/AdapterInput.ts b/ui/cypress/support/model/AdapterInput.ts new file mode 100644 index 0000000..73d54ee --- /dev/null +++ b/ui/cypress/support/model/AdapterInput.ts @@ -0,0 +1,5 @@ +export class AdapterInput { + adapterType: string; + adapterName: string; + timestampProperty: string; +} \ No newline at end of file diff --git a/ui/cypress/support/model/GenericAdapterInput.ts b/ui/cypress/support/model/GenericAdapterInput.ts new file mode 100644 index 0000000..793f26c --- /dev/null +++ b/ui/cypress/support/model/GenericAdapterInput.ts @@ -0,0 +1,8 @@ +import {UserInput} from './UserInput'; +import {AdapterInput} from './AdapterInput'; + +export class GenericAdapterInput extends AdapterInput{ + protocolConfiguration: UserInput[]; + format: string; + formatConfiguration: UserInput[]; +} \ No newline at end of file diff --git a/ui/cypress/support/model/PipelineElementInput.ts b/ui/cypress/support/model/PipelineElementInput.ts new file mode 100644 index 0000000..4c7a09b --- /dev/null +++ b/ui/cypress/support/model/PipelineElementInput.ts @@ -0,0 +1,6 @@ +import {UserInput} from './UserInput'; + +export class PipelineElementInput { + name: string; + config: UserInput[]; +} \ No newline at end of file diff --git a/ui/cypress/support/model/PipelineInput.ts b/ui/cypress/support/model/PipelineInput.ts new file mode 100644 index 0000000..f830fd1 --- /dev/null +++ b/ui/cypress/support/model/PipelineInput.ts @@ -0,0 +1,10 @@ +import {PipelineElementInput} from './PipelineElementInput'; + +export class PipelineInput { + pipelineName: string; + dataSource: string; + + processingElement: PipelineElementInput; + + dataSink: PipelineElementInput; +} \ No newline at end of file diff --git a/ui/cypress/support/model/SpecificAdapterInput.ts b/ui/cypress/support/model/SpecificAdapterInput.ts new file mode 100644 index 0000000..ffd3ab8 --- /dev/null +++ b/ui/cypress/support/model/SpecificAdapterInput.ts @@ -0,0 +1,6 @@ +import {UserInput} from './UserInput'; +import {AdapterInput} from './AdapterInput'; + +export class SpecificAdapterInput extends AdapterInput{ + adapterConfiguration: UserInput[]; +} \ No newline at end of file diff --git a/ui/cypress/support/model/UserInput.ts b/ui/cypress/support/model/UserInput.ts new file mode 100644 index 0000000..fbf1451 --- /dev/null +++ b/ui/cypress/support/model/UserInput.ts @@ -0,0 +1,5 @@ +export class UserInput { + type: string | 'checkbox' | 'input' | 'file' | 'drop-down' | 'radio'; + selector: string; + value: string; +} diff --git a/ui/cypress/support/utils/AdapterUtils.ts b/ui/cypress/support/utils/AdapterUtils.ts new file mode 100644 index 0000000..1a2152a --- /dev/null +++ b/ui/cypress/support/utils/AdapterUtils.ts @@ -0,0 +1,179 @@ +import {UserInput} from '../model/UserInput'; +import {StaticPropertyUtils} from './StaticPropertyUtils'; +import {SpecificAdapterInput} from '../model/SpecificAdapterInput'; +import {GenericAdapterInput} from '../model/GenericAdapterInput'; +import {SpecificAdapterBuilder} from '../builder/SpecificAdapterBuilder'; + +export class AdapterUtils { + + public static testSpecificStreamAdapter(adapterConfiguration: SpecificAdapterInput) { + + AdapterUtils.goToConnect(); + + AdapterUtils.selectAdapter(adapterConfiguration.adapterType); + + AdapterUtils.configureAdapter(adapterConfiguration.adapterConfiguration); + + AdapterUtils.finishEventSchemaConfiguration(); + + AdapterUtils.startStreamAdapter(adapterConfiguration.adapterName); + + AdapterUtils.deleteAdapter(); + + } + + public static testGenericStreamAdapter(adapterConfiguration: GenericAdapterInput) { + + AdapterUtils.addGenericStreamAdapter(adapterConfiguration); + + AdapterUtils.deleteAdapter(); + + } + + + public static addGenericStreamAdapter(adapterConfiguration: GenericAdapterInput) { + AdapterUtils.addGenericAdapter(adapterConfiguration); + + AdapterUtils.startStreamAdapter(adapterConfiguration.adapterName); + } + + public static addGenericSetAdapter(adapterConfiguration: GenericAdapterInput) { + AdapterUtils.addGenericAdapter(adapterConfiguration); + + AdapterUtils.startSetAdapter(adapterConfiguration.adapterName); + } + + private static addGenericAdapter(adapterConfiguration: GenericAdapterInput) { + AdapterUtils.goToConnect(); + + AdapterUtils.selectAdapter(adapterConfiguration.adapterType); + + AdapterUtils.configureAdapter(adapterConfiguration.protocolConfiguration); + + AdapterUtils.configureFormat(adapterConfiguration); + + AdapterUtils.markPropertyAsTimestamp(adapterConfiguration.timestampProperty); + + AdapterUtils.finishEventSchemaConfiguration(); + } + + public static addMachineDataSimulator(name: string) { + + const configuration = SpecificAdapterBuilder + .create('Machine_Data_Simulator') + .setName(name) + .addInput('input', 'wait-time-ms','1000') + .build(); + + AdapterUtils.goToConnect(); + + AdapterUtils.selectAdapter(configuration.adapterType); + + AdapterUtils.configureAdapter(configuration.adapterConfiguration); + + AdapterUtils.finishEventSchemaConfiguration(); + + AdapterUtils.startStreamAdapter(configuration.adapterName); + + } + + private static goToConnect() { + it('Login', function () { + cy.login(); + }); + + it('Go to StreamPipes connect', function () { + cy.visit('#/connect'); + }); + } + + private static selectAdapter(name) { + it('Select adapter', function () { + cy.get('#' + name).click(); + }); + } + + private static configureAdapter(configs: UserInput[]) { + it('Next Button should be disabled', function () { + cy.get('button').contains('Next').parent().should('be.disabled'); + }); + + StaticPropertyUtils.input(configs); + + it('Next Button should not be disabled', function () { + cy.get('button').contains('Next').parent().should('not.be.disabled'); + }); + + it('Click next', function () { + cy.get('button').contains('Next').parent().click(); + }); + } + + private static configureFormat(adapterConfiguration: GenericAdapterInput) { + it('Select format', function () { + cy.dataCy(adapterConfiguration.format).click(); + }); + + StaticPropertyUtils.input(adapterConfiguration.formatConfiguration); + + it('Click next', function () { + // cy.dataCy('sp-format-selection-next-button').parent().should('not.be.disabled'); + cy.dataCy('sp-format-selection-next-button').contains('Next').parent().click(); + }); + } + + private static markPropertyAsTimestamp(propertyName: string) { + it('Mark property as timestamp', function () { + + // Edit timestamp + cy.dataCy('edit-' + propertyName).click(); + + // Mark as timestamp + cy.dataCy('sp-mark-as-timestamp').children().click(); + + // Close + cy.dataCy('sp-save-edit-property').click(); + }); + } + + private static finishEventSchemaConfiguration() { + it('Click next', function () { + cy.get('[data-cy=sp-connect-schema-editor]', { timeout: 10000 }).should('be.visible'); + cy.get('#event-schema-next-button').click(); + }); + } + + private static startStreamAdapter(name) { + AdapterUtils.startAdapter(name, 'sp-connect-adapter-live-preview'); + } + + private static startSetAdapter(name) { + AdapterUtils.startAdapter(name, 'sp-connect-adapter-set-success'); + } + + private static startAdapter(name ,successElement) { + it('Set adapter name', function () { + cy.get('[data-cy=sp-adapter-name]').type(name); + }); + + it('Start adapter', function () { + cy.get('#button-startAdapter').click(); + cy.get('[data-cy=' + successElement + ']', { timeout: 10000 }).should('be.visible'); + }); + + it('Close adapter preview', function () { + cy.get('button').contains('Close').parent().click(); + }); + } + + public static deleteAdapter() { + it('Delete adapter', function () { + cy.visit('#/connect'); + + cy.get('div').contains('My Adapters').parent().click(); + cy.dataCy('delete').should('have.length', 1) + cy.dataCy('delete').click() + cy.get('[data-cy=delete]', { timeout: 10000 }).should('have.length', 0); + }); + } +} diff --git a/ui/cypress/support/utils/DataLakeUtils.ts b/ui/cypress/support/utils/DataLakeUtils.ts new file mode 100644 index 0000000..6d85802 --- /dev/null +++ b/ui/cypress/support/utils/DataLakeUtils.ts @@ -0,0 +1,34 @@ +import * as CSV from 'csv-string'; + +export class DataLakeUtils { + + public static checkResults(dataLakeIndex: string, fileRoute: string) { + + it('Validate result in datalake', function () { + const streamPipesUser = '[email protected]'; + cy.request('GET', '/streampipes-backend/api/v3/users/' + streamPipesUser + '/datalake/data/' + dataLakeIndex + '/download?format=csv', + {'content-type': 'application/octet-stream'}).should((response) => { + const expectedResultString = response.body; + cy.readFile(fileRoute).then((actualResultString) => { + DataLakeUtils.resultEqual(actualResultString, expectedResultString); + }); + }); + }); + } + + private static resultEqual(actual: string, expected: string) { + const expectedResult = DataLakeUtils.parseCsv(expected); + const actualResult = DataLakeUtils.parseCsv(actual); + expect(expectedResult).to.deep.equal(actualResult); + } + + private static parseCsv(csv: string) { + const result = [] + const index = CSV.readAll(csv, row => { + result.push(row); + }); + + return result; + } + +} \ No newline at end of file diff --git a/ui/cypress/support/utils/FileManagementUtils.ts b/ui/cypress/support/utils/FileManagementUtils.ts new file mode 100644 index 0000000..c9e5606 --- /dev/null +++ b/ui/cypress/support/utils/FileManagementUtils.ts @@ -0,0 +1,33 @@ + +export class FileManagementUtils { + + public static addFile(filePath: string) { + it('Go to StreamPipes file management', function () { + cy.visit('#/files'); + }); + + it('Open file upload dialog', function () { + cy.dataCy('sp-open-file-upload-dialog').click(); + }); + + it('Upload file', function () { + // const filepath = 'fileTest/test.csv' + cy.dataCy('sp-file-management-file-input').attachFile(filePath); + cy.dataCy('sp-file-management-store-file').click() + }); + } + + public static deleteFile() { + it('Go to StreamPipes file management', function () { + cy.visit('#/files'); + }); + it('Check if file was uploaded and delete it', function () { + cy.dataCy('delete').should('have.length', 1) + cy.dataCy('delete').click() + cy.dataCy('confirm-delete').click() + cy.dataCy('delete').should('have.length', 0) + }); + } + + +} \ No newline at end of file diff --git a/ui/cypress/support/utils/PipelineUtils.ts b/ui/cypress/support/utils/PipelineUtils.ts new file mode 100644 index 0000000..c22b304 --- /dev/null +++ b/ui/cypress/support/utils/PipelineUtils.ts @@ -0,0 +1,88 @@ +import {PipelineInput} from '../model/PipelineInput'; +import {UserInput} from '../model/UserInput'; +import {StaticPropertyUtils} from './StaticPropertyUtils'; + +export class PipelineUtils { + + public static testPipeline(pipelineInput: PipelineInput) { + + PipelineUtils.addPipeline(pipelineInput); + + PipelineUtils.deletePipeline(); + + } + + public static addPipeline(pipelineInput: PipelineInput) { + + PipelineUtils.goToPipelineEditor(); + + PipelineUtils.selectDataStream(pipelineInput); + + PipelineUtils.configurePipeline(pipelineInput); + + PipelineUtils.startPipeline(pipelineInput); + + } + + + private static goToPipelineEditor() { + it('Go to StreamPipes connect', function () { + cy.visit('#/editor'); + }); + } + + private static selectDataStream(pipelineInput: PipelineInput) { + it('Select a stream', function () { + cy.dataCy('sp-editor-add-pipeline-element').click(); + cy.dataCy(pipelineInput.dataSource).click(); + }); + } + + + private static configurePipeline(pipelineInput: PipelineInput) { + + it('Select processor', function () { + cy.get('[data-cy=sp-possible-elements-' + pipelineInput.dataSource +']', { timeout: 10000 }).click(); + cy.dataCy('sp-compatible-elements-' + pipelineInput.processingElement.name).click(); + }); + + StaticPropertyUtils.input(pipelineInput.processingElement.config); + + it('Save configuration', function () { + cy.dataCy('sp-element-configuration-save').click(); + }); + + it('Select sink', function () { + cy.dataCy('sp-possible-elements-' + pipelineInput.processingElement.name).click(); + cy.dataCy('sp-compatible-elements-'+ pipelineInput.dataSink.name).click(); + }); + + StaticPropertyUtils.input(pipelineInput.dataSink.config); + + it('Save sink configuration', function () { + cy.dataCy('sp-element-configuration-save').click(); + }); + + } + + private static startPipeline(pipelineInput: PipelineInput) { + it('Save and start pipeline', function () { + cy.dataCy('sp-editor-save-pipeline').click(); + cy.dataCy('sp-editor-pipeline-name').type(pipelineInput.pipelineName); + cy.dataCy('sp-editor-checkbox-start-immediately').children().click(); + cy.dataCy('sp-editor-save').click(); + cy.get('[data-cy=sp-pipeline-started-dialog]', { timeout: 10000 }).should('be.visible'); + cy.get('[data-cy=sp-pipeline-dialog-close]', { timeout: 10000 }).click(); + }); + } + + public static deletePipeline() { + it('Delete pipeline', function () { + cy.visit('#/pipelines'); + cy.dataCy('delete').should('have.length', 1) + cy.dataCy('delete').click() + cy.dataCy('sp-pipeline-stop-and-delete').click() + cy.get('[data-cy=delete]', { timeout: 10000 }).should('have.length', 0); + }); + } +} diff --git a/ui/cypress/support/utils/ProcessingElementTestUtils.ts b/ui/cypress/support/utils/ProcessingElementTestUtils.ts new file mode 100644 index 0000000..967b3ab --- /dev/null +++ b/ui/cypress/support/utils/ProcessingElementTestUtils.ts @@ -0,0 +1,58 @@ +import {FileManagementUtils} from './FileManagementUtils'; +import {AdapterUtils} from './AdapterUtils'; +import {PipelineElementInput} from '../model/PipelineElementInput'; +import {PipelineUtils} from './PipelineUtils'; +import {DataLakeUtils} from './DataLakeUtils'; +import {GenericAdapterBuilder} from '../builder/GenericAdapterBuilder'; +import {PipelineBuilder} from '../builder/PipelineBuilder'; +import {PipelineElementBuilder} from '../builder/PipelineElementBuilder'; + +export class ProcessingElementTestUtils { + + public static testElement(testName: string, inputFile: string, expectedResultFile: string, processor: PipelineElementInput) { + // Test + FileManagementUtils.addFile(inputFile); + + const dataLakeIndex = testName.toLowerCase(); + + const adapterName = testName.toLowerCase(); + + // Build adapter + const adapterInput = GenericAdapterBuilder + .create('File_Set') + .setName(adapterName) + .setTimestampProperty('timestamp') + .addProtocolInput('input', 'interval-key','0') + .setFormat('csv') + .addFormatInput('input', 'delimiter',';') + .addFormatInput('checkbox', 'header','check') + .build(); + + AdapterUtils.addGenericSetAdapter(adapterInput); + + // Build Pipeline + const pipelineInput = PipelineBuilder.create(testName) + .addSource(adapterName) + .addProcessingElement(processor) + .addSink( + PipelineElementBuilder.create('data_lake') + .addInput('input', 'db_measurement',dataLakeIndex) + .build()) + .build(); + + PipelineUtils.addPipeline(pipelineInput); + + // // Wait + it('Wait till data is stored', function () { + cy.wait(10000) + }); + + DataLakeUtils.checkResults(dataLakeIndex, 'cypress/fixtures/' + expectedResultFile); + + PipelineUtils.deletePipeline(); + + AdapterUtils.deleteAdapter(); + + FileManagementUtils.deleteFile(); + } +} \ No newline at end of file diff --git a/ui/cypress/support/utils/StaticPropertyUtils.ts b/ui/cypress/support/utils/StaticPropertyUtils.ts new file mode 100644 index 0000000..7fc414f --- /dev/null +++ b/ui/cypress/support/utils/StaticPropertyUtils.ts @@ -0,0 +1,21 @@ +import {UserInput} from '../model/UserInput'; + +export class StaticPropertyUtils { + + public static input(configs: UserInput[]) { + + it('Configure Properties', function () { + configs.forEach(config => { + if (config.type == 'checkbox') { + cy.dataCy(config.selector).children().click(); + } else if (config.type == 'drop-down') { + cy.dataCy(config.selector).click().get('mat-option').contains(config.value).click(); + } else if (config.type == 'radio') { + cy.dataCy(config.selector + config.value ).click(); + } else { + cy.dataCy(config.selector).type(config.value); + } + }); + }); + } +} diff --git a/ui/cypress/tsconfig.json b/ui/cypress/tsconfig.json new file mode 100644 index 0000000..abba62d --- /dev/null +++ b/ui/cypress/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress"] + }, + "include": [ + "**/*.ts", + "node_modules/@types/node/globals.d.ts" + ] +} \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index efc485d..a0f6651 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,8 @@ "build": "node ./deployment/prebuild.js && set NODE_OPTIONS=--max-old-space-size=10192 && ng build --prod", "build-rc": "node ./deployment/prebuild.js rel && set NODE_OPTIONS=--max-old-space-size=10192 && ng build --prod", "lint": "tslint -c tslint.json 'src/**/*.ts*", - "analyze": "webpack-bundle-analyzer dist/stats.json" + "analyze": "webpack-bundle-analyzer dist/stats.json", + "test-e2e": "cypress open" }, "dependencies": { "@angular/animations": "11.0.5", @@ -106,6 +107,9 @@ "compression-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^5.1.2", "css-loader": "^0.28.11", + "csv-string": "4.0.1", + "cypress": "^7.5.0", + "cypress-file-upload": "5.0.7", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.6", "html-webpack-plugin": "^3.2.0",
