This is an automated email from the ASF dual-hosted git repository. zehnder pushed a commit to branch improve-tests in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 2a6e1abc341c92c4841bc5b095f536ad8d43ee7b Author: Philipp Zehnder <[email protected]> AuthorDate: Thu Feb 26 16:10:33 2026 +0100 feat: enhance e2e tests --- ui/cypress/AGENTS.md | 53 ++++++++++ ui/cypress/support/utils/asset/AssetUtils.ts | 4 +- .../support/utils/dataExplorer/DataExplorerBtns.ts | 48 +++++++++ .../utils/dataExplorer/DataExplorerUtils.ts | 25 ++--- 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, 275 insertions(+), 111 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..5528d03f5b 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'; @@ -101,7 +100,7 @@ export class DataExplorerUtils { 'speed', 'fastest_\\(ignore_original_time\\)', ) - .setStartAdapter(true); + .setStartAdapter(false); if (format === 'csv') { adapterBuilder @@ -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(); } }
