This is an automated email from the ASF dual-hosted git repository.
zehnder pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git
The following commit(s) were added to refs/heads/dev by this push:
new 5ac882a0d1 feat: enhance e2e tests (#4204)
5ac882a0d1 is described below
commit 5ac882a0d18c49929a3cb6de8e6b6490e5486659
Author: Philipp Zehnder <[email protected]>
AuthorDate: Fri Feb 27 08:33:57 2026 +0100
feat: enhance e2e tests (#4204)
---
ui/cypress/AGENTS.md | 53 ++++++++++
ui/cypress/support/utils/asset/AssetUtils.ts | 4 +-
.../support/utils/dataExplorer/DataExplorerBtns.ts | 48 +++++++++
.../utils/dataExplorer/DataExplorerUtils.ts | 23 ++---
ui/cypress/support/utils/pipeline/PipelineBtns.ts | 40 ++++++++
.../advancedFilterExpressions.smoke.spec.ts | 39 ++++----
.../tests/pipeline/pipelineMultiSelect.spec.ts | 110 +++++++--------------
.../components/chart-view/chart-view.component.ts | 67 ++++++++++++-
8 files changed, 274 insertions(+), 110 deletions(-)
diff --git a/ui/cypress/AGENTS.md b/ui/cypress/AGENTS.md
new file mode 100644
index 0000000000..9bedcd1a9a
--- /dev/null
+++ b/ui/cypress/AGENTS.md
@@ -0,0 +1,53 @@
+# AGENTS Guide (Cypress E2E)
+
+## Scope
+
+Applies to everything under `ui/cypress/`.
+
+## Inheritance
+
+- Also follow `AGENTS.md` at repository root.
+- Also follow `ui/AGENTS.md`.
+
+## Primary Goal
+
+Keep tests readable and maintainable by centralizing selectors and common
flows in `support/` classes.
+
+## Authoring Rules For New/Generated Tests
+
+- Prefer existing helpers from `ui/cypress/support/utils/**` and
`ui/cypress/support/builder/**`.
+- Do not introduce new inline selector strings in spec files when a selector
can be reused.
+- If a new selector is needed, add it to a fitting support class first, then
consume it from the spec.
+- Keep specs focused on scenario intent and assertions, not low-level UI
wiring.
+
+## Selector Placement Rules
+
+- Put `data-cy` element accessors in domain `*Btns` classes (for example
`PipelineBtns`, `ConnectBtns`, `DataExplorerBtns`).
+- Put multi-step user flows in domain `*Utils` classes (for example
`PipelineUtils`, `ConnectUtils`, `DataExplorerUtils`).
+- For dynamic selectors, use typed helper methods with parameters instead of
string concatenation in specs.
+- Reuse existing selector constants/patterns when already present (for example
`SiteUtils` constants).
+- Keep direct `cy.get(...)` in specs to a minimum; if reused, move it behind a
support helper.
+
+## Spec Structure
+
+- Initialize test state with `cy.initStreamPipesTest()` unless a test
explicitly requires different setup.
+- Reuse builders for test objects (`AdapterBuilder`, `PipelineBuilder`,
`PipelineElementBuilder`, ...).
+- Avoid fixed `cy.wait(...)` where possible; prefer state-based
waits/assertions via helpers.
+- Keep each test independent: no inter-test dependencies.
+
+## Refactoring Expectations
+
+- When touching an existing spec with many inline selectors, opportunistically
move touched selectors to the matching `*Btns`/`*Utils` class.
+- Do not do broad cross-module rewrites; keep refactors scoped to the test
area being changed.
+
+## Naming And Organization
+
+- Follow existing naming style:
+ - UI accessors: verb/noun methods in `*Btns` returning Cypress chains.
+ - Flows/assertions: descriptive methods in `*Utils`.
+- Place new helpers in the closest domain folder under `support/utils/`
(connect, pipeline, dataExplorer, ...).
+
+## Validation
+
+- For Cypress changes, run targeted specs when feasible (for example via smoke
selection).
+- If execution is not possible, document what was not run.
diff --git a/ui/cypress/support/utils/asset/AssetUtils.ts
b/ui/cypress/support/utils/asset/AssetUtils.ts
index 8444e391bd..a408bd8418 100644
--- a/ui/cypress/support/utils/asset/AssetUtils.ts
+++ b/ui/cypress/support/utils/asset/AssetUtils.ts
@@ -158,9 +158,7 @@ export class AssetUtils {
public static editAsset(assetName: string) {
GeneralUtils.openMenuForRow(assetName);
- cy.contains('button', 'Edit').click({ force: true });
- //This is the old version and there in case above does not work for
all tests
- //AssetBtns.editAssetBtn(assetName).click({ force: true });
+ AssetBtns.editAssetBtn(assetName).click({ force: true });
}
public static addAssetWithOneAdapter(assetName: string) {
diff --git a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
index f644453904..ee99646fe0 100644
--- a/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
+++ b/ui/cypress/support/utils/dataExplorer/DataExplorerBtns.ts
@@ -157,4 +157,52 @@ export class DataExplorerBtns {
public static closeDashboardCreate() {
return cy.dataCy('close-data-view');
}
+
+ public static advancedFilterBtn() {
+ return cy.dataCy('design-panel-data-settings-advanced-filter');
+ }
+
+ public static advancedFilterAddConditionBtn() {
+ return cy.dataCy('advanced-filter-add-condition');
+ }
+
+ public static advancedFilterAddGroupBtn() {
+ return cy.dataCy('advanced-filter-add-group');
+ }
+
+ public static advancedFilterGroupOperator() {
+ return cy.dataCy('advanced-filter-group-operator', {}, true);
+ }
+
+ public static advancedFilterPreviewBanner() {
+ return cy.dataCy('advanced-filter-preview-banner');
+ }
+
+ public static advancedFilterApplyBtn() {
+ return cy.dataCy('advanced-filter-apply');
+ }
+
+ public static filterAlertBanner() {
+ return cy.dataCy('filter-alert-banner', { timeout: 2000 });
+ }
+
+ public static filterFieldSelect() {
+ return cy.dataCy('design-panel-data-settings-filter-field', {}, true);
+ }
+
+ public static filterOperatorSelect() {
+ return cy.dataCy(
+ 'design-panel-data-settings-filter-operator',
+ {},
+ true,
+ );
+ }
+
+ public static filterValueInput() {
+ return cy.dataCy('design-panel-data-settings-filter-value', {}, true);
+ }
+
+ public static matOptionByText(text: string | RegExp) {
+ return cy.get('mat-option').contains(text);
+ }
}
diff --git a/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
b/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
index e918092423..caea6cb0cf 100644
--- a/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
+++ b/ui/cypress/support/utils/dataExplorer/DataExplorerUtils.ts
@@ -24,7 +24,6 @@ import { FileManagementUtils } from '../FileManagementUtils';
import { ConnectUtils } from '../connect/ConnectUtils';
import { ConnectBtns } from '../connect/ConnectBtns';
import { AdapterBuilder } from '../../builder/AdapterBuilder';
-import { differenceInMonths } from 'date-fns';
import { GeneralUtils } from '../GeneralUtils';
import { DataExplorerBtns } from './DataExplorerBtns';
import { SharedBtns } from '../shared/SharedBtns';
@@ -659,19 +658,21 @@ export class DataExplorerUtils {
}
public static selectTimeRange(from: Date, to: Date) {
- DataExplorerUtils.openTimeSelectorMenu();
- const monthsBack = Math.abs(differenceInMonths(from, new Date())) + 1;
- DataExplorerUtils.navigateCalendar('previous', monthsBack);
- DataExplorerUtils.selectDay(from.getDate());
+ cy.location('hash').then(hash => {
+ const [route, queryString] = hash.split('?');
+ const searchParams = new URLSearchParams(queryString ?? '');
- const monthsForward = Math.abs(differenceInMonths(from, to));
- DataExplorerUtils.navigateCalendar('next', monthsForward);
+ searchParams.set('startDate', from.getTime().toString());
+ searchParams.set('endDate', to.getTime().toString());
- DataExplorerUtils.selectDay(to.getDate());
+ const updatedHash = `${route}?${searchParams.toString()}`;
+ cy.window().then(win => {
+ win.location.hash = updatedHash;
+ });
+ });
- DataExplorerUtils.setTimeInput('time-selector-start-time', from);
- DataExplorerUtils.setTimeInput('time-selector-end-time', to);
- DataExplorerUtils.applyCustomTimeSelection();
+ cy.location('hash').should('contain', `startDate=${from.getTime()}`);
+ cy.location('hash').should('contain', `endDate=${to.getTime()}`);
}
public static navigateCalendar(direction: string, numberOfMonths: number) {
diff --git a/ui/cypress/support/utils/pipeline/PipelineBtns.ts
b/ui/cypress/support/utils/pipeline/PipelineBtns.ts
index 1ba9c36db7..8f57da5995 100644
--- a/ui/cypress/support/utils/pipeline/PipelineBtns.ts
+++ b/ui/cypress/support/utils/pipeline/PipelineBtns.ts
@@ -101,4 +101,44 @@ export class PipelineBtns {
public static pipelineHelpBtn() {
return cy.dataCy('help-button-icon-stand');
}
+
+ public static selectionToolbar() {
+ return cy.dataCy('sp-table-selection-toolbar');
+ }
+
+ public static rowCheckbox() {
+ return cy.dataCy('sp-table-row-checkbox');
+ }
+
+ public static rowCheckboxInput(index: number) {
+ return PipelineBtns.rowCheckbox()
+ .eq(index)
+ .find('input[type="checkbox"]');
+ }
+
+ public static multiActionExecute() {
+ return cy.dataCy('sp-table-multi-action-execute');
+ }
+
+ public static selectNone() {
+ return cy.dataCy('sp-table-select-none');
+ }
+
+ public static multiActionSelect() {
+ return cy.dataCy('sp-table-multi-action-select');
+ }
+
+ public static multiActionOptionStop() {
+ return cy.dataCy('sp-table-multi-action-option-stop');
+ }
+
+ public static selectVisible() {
+ return cy.dataCy('sp-table-select-visible');
+ }
+
+ public static selectAllCheckboxInput() {
+ return cy
+ .dataCy('sp-table-select-all-checkbox')
+ .find('input[type="checkbox"]');
+ }
}
diff --git
a/ui/cypress/tests/dataExplorer/advancedFilterExpressions.smoke.spec.ts
b/ui/cypress/tests/dataExplorer/advancedFilterExpressions.smoke.spec.ts
index 2231c63e5b..07e618ef30 100644
--- a/ui/cypress/tests/dataExplorer/advancedFilterExpressions.smoke.spec.ts
+++ b/ui/cypress/tests/dataExplorer/advancedFilterExpressions.smoke.spec.ts
@@ -17,6 +17,7 @@
*/
import { DataExplorerUtils } from
'../../support/utils/dataExplorer/DataExplorerUtils';
+import { DataExplorerBtns } from
'../../support/utils/dataExplorer/DataExplorerBtns';
import { DataExplorerWidgetTableUtils } from
'../../support/utils/dataExplorer/DataExplorerWidgetTableUtils';
describe('Advanced Filter Expressions in Data Explorer', () => {
@@ -34,21 +35,21 @@ describe('Advanced Filter Expressions in Data Explorer', ()
=> {
DataExplorerWidgetTableUtils.checkAmountOfRows(10);
DataExplorerUtils.selectDataConfig();
- cy.dataCy('design-panel-data-settings-advanced-filter').click();
+ DataExplorerBtns.advancedFilterBtn().click();
// Root condition: randomtext = a
- cy.dataCy('advanced-filter-add-condition').first().click();
+ DataExplorerBtns.advancedFilterAddConditionBtn().first().click();
// Root nested group
- cy.dataCy('advanced-filter-add-group').first().click();
+ DataExplorerBtns.advancedFilterAddGroupBtn().first().click();
// Two conditions in nested group
- cy.dataCy('advanced-filter-add-condition').last().click();
- cy.dataCy('advanced-filter-add-condition').last().click();
+ DataExplorerBtns.advancedFilterAddConditionBtn().last().click();
+ DataExplorerBtns.advancedFilterAddConditionBtn().last().click();
// Set nested group operator to OR
- cy.dataCy('advanced-filter-group-operator', {}, true)
+ DataExplorerBtns.advancedFilterGroupOperator()
.last()
.click({ force: true });
- cy.get('mat-option').contains(/^OR$/).click();
+ DataExplorerBtns.matOptionByText(/^OR$/).click();
setAdvancedCondition(0, 'randomtext', '=', 'a');
setAdvancedCondition(1, 'randomnumber', '=', '22');
@@ -57,13 +58,13 @@ describe('Advanced Filter Expressions in Data Explorer', ()
=> {
// Value inputs update the model on change; blur the active input
before checking the preview.
cy.focused().blur();
- cy.dataCy('advanced-filter-preview-banner')
+ DataExplorerBtns.advancedFilterPreviewBanner()
.should('contain.text', 'randomtext = a')
.and('contain.text', 'randomnumber = 22')
.and('contain.text', 'randomnumber = 56')
.and('contain.text', 'OR');
- cy.dataCy('advanced-filter-apply').click();
+ DataExplorerBtns.advancedFilterApplyBtn().click();
// a AND (22 OR 56) => 2 rows in sample.csv
DataExplorerWidgetTableUtils.checkAmountOfRows(2);
@@ -72,10 +73,8 @@ describe('Advanced Filter Expressions in Data Explorer', ()
=> {
DataExplorerWidgetTableUtils.checkAmountOfRows(2);
DataExplorerUtils.selectDataConfig();
- cy.dataCy('design-panel-data-settings-advanced-filter').should(
- 'be.visible',
- );
- cy.dataCy('filter-alert-banner', { timeout: 2000 })
+ DataExplorerBtns.advancedFilterBtn().should('be.visible');
+ DataExplorerBtns.filterAlertBanner()
.should('be.visible')
.within(() => {
cy.contains('randomtext = a');
@@ -89,17 +88,13 @@ function setAdvancedCondition(
operator: '=' | '!=' | '<' | '<=' | '>' | '>=',
value: string,
) {
- cy.dataCy('design-panel-data-settings-filter-field', {}, true)
- .eq(index)
- .click({ force: true });
- cy.get('mat-option').contains(field).click();
+ DataExplorerBtns.filterFieldSelect().eq(index).click({ force: true });
+ DataExplorerBtns.matOptionByText(field).click();
- cy.dataCy('design-panel-data-settings-filter-operator', {}, true)
- .eq(index)
- .click({ force: true });
- cy.get('mat-option').contains(operator).click();
+ DataExplorerBtns.filterOperatorSelect().eq(index).click({ force: true });
+ DataExplorerBtns.matOptionByText(operator).click();
- cy.dataCy('design-panel-data-settings-filter-value', {}, true)
+ DataExplorerBtns.filterValueInput()
.eq(index)
.clear({ force: true })
.type(value, { force: true });
diff --git a/ui/cypress/tests/pipeline/pipelineMultiSelect.spec.ts
b/ui/cypress/tests/pipeline/pipelineMultiSelect.spec.ts
index eacdb8e435..5caf26ac11 100644
--- a/ui/cypress/tests/pipeline/pipelineMultiSelect.spec.ts
+++ b/ui/cypress/tests/pipeline/pipelineMultiSelect.spec.ts
@@ -18,6 +18,7 @@
import { ConnectUtils } from '../../support/utils/connect/ConnectUtils';
import { PipelineUtils } from '../../support/utils/pipeline/PipelineUtils';
+import { PipelineBtns } from '../../support/utils/pipeline/PipelineBtns';
import { PipelineBuilder } from '../../support/builder/PipelineBuilder';
import { PipelineElementBuilder } from
'../../support/builder/PipelineElementBuilder';
@@ -54,78 +55,41 @@ describe('Pipeline Overview Multi Select', () => {
});
it('supports selecting rows and bulk action state changes', () => {
- cy.dataCy('sp-table-selection-toolbar').should('be.visible');
- cy.dataCy('sp-table-row-checkbox').should('have.length', 2);
-
- cy.dataCy('sp-table-multi-action-execute').should('be.disabled');
- cy.dataCy('sp-table-select-none').should('be.disabled');
-
- cy.dataCy('sp-table-row-checkbox')
- .eq(0)
- .find('input[type="checkbox"]')
- .check({ force: true });
- cy.dataCy('sp-table-select-none').should('not.be.disabled');
- cy.dataCy('sp-table-multi-action-execute').should('be.disabled');
-
- cy.dataCy('sp-table-multi-action-select').click();
- cy.dataCy('sp-table-multi-action-option-stop').click();
- cy.dataCy('sp-table-multi-action-execute').should('not.be.disabled');
-
- cy.dataCy('sp-table-row-checkbox')
- .eq(0)
- .find('input')
- .should('be.checked');
- cy.dataCy('sp-table-row-checkbox')
- .eq(1)
- .find('input')
- .should('not.be.checked');
-
- cy.dataCy('sp-table-select-visible').click();
- cy.dataCy('sp-table-row-checkbox')
- .eq(0)
- .find('input')
- .should('be.checked');
- cy.dataCy('sp-table-row-checkbox')
- .eq(1)
- .find('input')
- .should('be.checked');
- cy.dataCy('sp-table-multi-action-execute').should('not.be.disabled');
-
- cy.dataCy('sp-table-select-none').click();
- cy.dataCy('sp-table-row-checkbox')
- .eq(0)
- .find('input')
- .should('not.be.checked');
- cy.dataCy('sp-table-row-checkbox')
- .eq(1)
- .find('input')
- .should('not.be.checked');
- cy.dataCy('sp-table-multi-action-execute').should('be.disabled');
-
- cy.dataCy('sp-table-select-all-checkbox')
- .find('input[type="checkbox"]')
- .check({ force: true });
- cy.dataCy('sp-table-row-checkbox')
- .eq(0)
- .find('input')
- .should('be.checked');
- cy.dataCy('sp-table-row-checkbox')
- .eq(1)
- .find('input')
- .should('be.checked');
- cy.dataCy('sp-table-multi-action-execute').should('not.be.disabled');
-
- cy.dataCy('sp-table-select-all-checkbox')
- .find('input[type="checkbox"]')
- .uncheck({ force: true });
- cy.dataCy('sp-table-row-checkbox')
- .eq(0)
- .find('input')
- .should('not.be.checked');
- cy.dataCy('sp-table-row-checkbox')
- .eq(1)
- .find('input')
- .should('not.be.checked');
- cy.dataCy('sp-table-multi-action-execute').should('be.disabled');
+ PipelineBtns.selectionToolbar().should('be.visible');
+ PipelineBtns.rowCheckbox().should('have.length', 2);
+
+ PipelineBtns.multiActionExecute().should('be.disabled');
+ PipelineBtns.selectNone().should('be.disabled');
+
+ PipelineBtns.rowCheckboxInput(0).check({ force: true });
+ PipelineBtns.selectNone().should('not.be.disabled');
+ PipelineBtns.multiActionExecute().should('be.disabled');
+
+ PipelineBtns.multiActionSelect().click();
+ PipelineBtns.multiActionOptionStop().click();
+ PipelineBtns.multiActionExecute().should('not.be.disabled');
+
+ PipelineBtns.rowCheckboxInput(0).should('be.checked');
+ PipelineBtns.rowCheckboxInput(1).should('not.be.checked');
+
+ PipelineBtns.selectVisible().click();
+ PipelineBtns.rowCheckboxInput(0).should('be.checked');
+ PipelineBtns.rowCheckboxInput(1).should('be.checked');
+ PipelineBtns.multiActionExecute().should('not.be.disabled');
+
+ PipelineBtns.selectNone().click();
+ PipelineBtns.rowCheckboxInput(0).should('not.be.checked');
+ PipelineBtns.rowCheckboxInput(1).should('not.be.checked');
+ PipelineBtns.multiActionExecute().should('be.disabled');
+
+ PipelineBtns.selectAllCheckboxInput().check({ force: true });
+ PipelineBtns.rowCheckboxInput(0).should('be.checked');
+ PipelineBtns.rowCheckboxInput(1).should('be.checked');
+ PipelineBtns.multiActionExecute().should('not.be.disabled');
+
+ PipelineBtns.selectAllCheckboxInput().uncheck({ force: true });
+ PipelineBtns.rowCheckboxInput(0).should('not.be.checked');
+ PipelineBtns.rowCheckboxInput(1).should('not.be.checked');
+ PipelineBtns.multiActionExecute().should('be.disabled');
});
});
diff --git a/ui/src/app/chart/components/chart-view/chart-view.component.ts
b/ui/src/app/chart/components/chart-view/chart-view.component.ts
index 43a3461383..10c442c48b 100644
--- a/ui/src/app/chart/components/chart-view/chart-view.component.ts
+++ b/ui/src/app/chart/components/chart-view/chart-view.component.ts
@@ -31,6 +31,7 @@ import {
EventPropertyUnion,
FieldConfig,
LinkageData,
+ TimeSelectionConstants,
TimeSettings,
} from '@streampipes/platform-services';
import {
@@ -130,6 +131,7 @@ export class ChartViewComponent
private assetSaveService = inject(AssetSaveService);
currentUser$: Subscription;
+ queryParams$: Subscription;
chartNotFound = false;
@@ -153,10 +155,16 @@ export class ChartViewComponent
this.loadDataView(dataViewId);
} else {
this.createWidget();
- this.timeSettings = this.makeDefaultTimeSettings();
+ this.timeSettings =
+ this.getTimeSettingsFromQueryParams() ??
+ this.makeDefaultTimeSettings();
this.dataView.timeSettings = this.timeSettings;
this.afterDataViewLoaded();
}
+
+ this.queryParams$ = this.route.queryParams.subscribe(queryParams => {
+ this.applyTimeSettingsFromQueryParams(queryParams);
+ });
}
onAddWidget(event: Tuple2<DataLakeMeasure, DataExplorerWidgetModel>) {
@@ -225,11 +233,67 @@ export class ChartViewComponent
this.dataExplorerSharedService.makeChartTimeSettings(
this.dataView,
);
+ this.timeSettings =
+ this.getTimeSettingsFromQueryParams() ??
+ this.timeSettings;
this.afterDataViewLoaded();
}
});
}
+ private applyTimeSettingsFromQueryParams(queryParams: {
+ [key: string]: any;
+ }): void {
+ if (!this.timeSettings) {
+ return;
+ }
+
+ const startDate = Number(queryParams.startDate);
+ const endDate = Number(queryParams.endDate);
+ if (
+ !Number.isFinite(startDate) ||
+ !Number.isFinite(endDate) ||
+ startDate >= endDate
+ ) {
+ return;
+ }
+
+ if (
+ this.timeSettings.startTime === startDate &&
+ this.timeSettings.endTime === endDate
+ ) {
+ return;
+ }
+
+ this.timeSettings = {
+ ...this.timeSettings,
+ startTime: startDate,
+ endTime: endDate,
+ timeSelectionId: TimeSelectionConstants.CUSTOM,
+ };
+ this.timeSelectionService.notify(this.timeSettings);
+ }
+
+ private getTimeSettingsFromQueryParams(): TimeSettings | undefined {
+ const startDate = Number(this.route.snapshot.queryParams.startDate);
+ const endDate = Number(this.route.snapshot.queryParams.endDate);
+
+ if (
+ !Number.isFinite(startDate) ||
+ !Number.isFinite(endDate) ||
+ startDate >= endDate
+ ) {
+ return undefined;
+ }
+
+ return {
+ startTime: startDate,
+ endTime: endDate,
+ dynamicSelection: -1,
+ timeSelectionId: TimeSelectionConstants.CUSTOM,
+ };
+ }
+
afterDataViewLoaded(): void {
this.dataViewLoaded = true;
setTimeout(() => {
@@ -452,5 +516,6 @@ export class ChartViewComponent
ngOnDestroy() {
this.currentUser$?.unsubscribe();
+ this.queryParams$?.unsubscribe();
}
}