@@ -18,6 +18,23 @@ const _resetSearchState = () => {
1818 _quietSearch = false ;
1919} ;
2020
21+ // jQuery selector for the Incidents tab — covers both PatternFly v6 and
22+ // legacy Console horizontal nav markup. Used by goTo()/warmUpForPlugin()
23+ // to poll for plugin registration without Cypress command overhead.
24+ const _INCIDENTS_TAB_SELECTOR =
25+ '.pf-v6-c-tabs__item:contains("Incidents"), ' +
26+ '.co-m-horizontal-nav__menu-item:contains("Incidents")' ;
27+
28+ // Selector for bar group containers in the incidents chart (one per incident).
29+ const _BAR_GROUP_SELECTOR = 'g[role="presentation"][data-test*="incidents-chart-bar-"]' ;
30+
31+ // Filter predicate for visible path segments (fill-opacity > 0).
32+ // Multi-severity incidents have placeholder paths with zero opacity.
33+ const _isVisiblePath = ( _ : number , el : HTMLElement ) => {
34+ const opacity = Cypress . $ ( el ) . css ( 'fill-opacity' ) || Cypress . $ ( el ) . attr ( 'fill-opacity' ) ;
35+ return parseFloat ( opacity || '0' ) > 0 ;
36+ } ;
37+
2138export const incidentsPage = {
2239 // Centralized element selectors - all selectors defined in one place
2340 elements : {
@@ -93,11 +110,10 @@ export const incidentsPage = {
93110 // or conditional testing semantics.
94111 cy . wait ( 500 , _qLog ( ) ) ;
95112 // We need to use the $body as both cases when the element is there or not are valid.
96- const exists =
97- $body . find ( 'g[role="presentation"][data-test*="incidents-chart-bar-"]' ) . length > 0 ;
113+ const exists = $body . find ( _BAR_GROUP_SELECTOR ) . length > 0 ;
98114 if ( exists ) {
99115 return cy
100- . get ( 'g[role="presentation"][data-test*="incidents-chart-bar-"]' , _qLog ( ) )
116+ . get ( _BAR_GROUP_SELECTOR , _qLog ( ) )
101117 . find ( 'path[role="presentation"]' )
102118 . filter ( ( index , element ) => {
103119 const fillOpacity =
@@ -123,9 +139,7 @@ export const incidentsPage = {
123139 } ) ;
124140 } ,
125141 incidentsChartBarsGroups : ( ) =>
126- cy
127- . byTestID ( DataTestIDs . IncidentsChart . ChartBars )
128- . find ( 'g[role="presentation"][data-test*="incidents-chart-bar-"]' ) ,
142+ cy . byTestID ( DataTestIDs . IncidentsChart . ChartBars ) . find ( _BAR_GROUP_SELECTOR ) ,
129143 incidentsChartSvg : ( ) => incidentsPage . elements . incidentsChartCard ( ) . find ( 'svg' ) ,
130144
131145 alertsChartTitle : ( ) => cy . byTestID ( DataTestIDs . AlertsChart . Title ) ,
@@ -212,8 +226,11 @@ export const incidentsPage = {
212226 } ,
213227
214228 goTo : ( ) => {
215- cy . log ( 'incidentsPage.goTo' ) ;
229+ if ( ! _quietSearch ) cy . log ( 'incidentsPage.goTo' ) ;
216230 nav . sidenav . clickNavLink ( [ 'Observe' , 'Alerting' ] ) ;
231+ // Wait for the Incidents tab to be registered by the dynamic plugin.
232+ // After session restore the plugin may need up to 2 min to re-register.
233+ incidentsPage . waitForIncidentsTab ( ) ;
217234 nav . tabs . switchTab ( 'Incidents' ) ;
218235 incidentsPage . elements . daysSelectToggle ( ) . should ( 'be.visible' ) ;
219236 } ,
@@ -228,19 +245,20 @@ export const incidentsPage = {
228245 // Wait up to 3 minutes for the Incidents tab to appear. Uses synchronous jQuery check
229246 // inside cy.waitUntil() to avoid the 80s default command timeout, then uses
230247 // nav.tabs.switchTab() which correctly clicks the button element (not the li wrapper).
231- cy . waitUntil (
232- ( ) =>
233- Cypress . $ (
234- '.pf-v6-c-tabs__item:contains("Incidents"), .co-m-horizontal-nav__menu-item:contains("Incidents")' ,
235- ) . length > 0 ,
236- {
237- interval : 3000 ,
238- timeout : 180000 ,
239- errorMsg : 'Incidents tab not registered within 3 minutes' ,
240- } ,
241- ) ;
248+ incidentsPage . waitForIncidentsTab ( ) ;
242249 nav . tabs . switchTab ( 'Incidents' ) ;
243- cy . get ( '[data-test="incidents-days-select-toggle"]' , { timeout : 180000 } ) . should ( 'be.visible' ) ;
250+ incidentsPage . elements . daysSelectToggle ( ) . should ( 'be.visible' ) ;
251+ } ,
252+
253+ // Polls for the Incidents tab to appear in the horizontal nav using a
254+ // synchronous jQuery check (no Cypress command overhead / DOM snapshots).
255+ // Shared by goTo() and warmUpForPlugin().
256+ waitForIncidentsTab : ( ) => {
257+ cy . waitUntil ( ( ) => Cypress . $ ( _INCIDENTS_TAB_SELECTOR ) . length > 0 , {
258+ interval : 2000 ,
259+ timeout : 180000 ,
260+ errorMsg : 'Incidents tab not registered within 3 minutes' ,
261+ } ) ;
244262 } ,
245263
246264 setDays : ( value : '1 day' | '3 days' | '7 days' | '15 days' ) => {
@@ -399,42 +417,39 @@ export const incidentsPage = {
399417 } ,
400418
401419 /**
402- * Selects an incident from the chart by clicking on a bar at the specified index.
403- * BUG: Problems with multi-severity incidents (multiple paths in a single incident bar)
420+ * Selects an incident from the chart by clicking on a bar group at the
421+ * specified index. Uses bar groups (one per incident) instead of flattened
422+ * paths to correctly handle multi-severity incidents.
404423 *
405424 * @param index - Zero-based index of the incident bar to click (default: 0)
406425 * @returns Promise that resolves when the incidents table is visible
407426 */
408427 selectIncidentByBarIndex : ( index = 0 ) => {
409- if ( ! _quietSearch )
410- cy . log ( `incidentsPage.selectIncidentByBarIndex: ${ index } (clicking visible path elements)` ) ;
428+ if ( ! _quietSearch ) cy . log ( `incidentsPage.selectIncidentByBarIndex: ${ index } ` ) ;
411429
412430 return incidentsPage . elements
413- . incidentsChartBarsVisiblePaths ( )
431+ . incidentsChartBarsGroups ( )
414432 . should ( 'have.length.greaterThan' , index )
415- . then ( ( $paths ) => {
416- if ( index >= $paths . length ) {
417- throw new Error ( `Index ${ index } exceeds available paths (${ $paths . length } )` ) ;
418- }
419-
420- return cy . wrap ( $paths . eq ( index ) , _qLog ( ) ) . click ( { force : true , ..._qLog ( ) } ) ;
421- } )
433+ . eq ( index )
434+ . find ( 'path[role="presentation"]' )
435+ . filter ( _isVisiblePath )
436+ . first ( )
437+ . click ( { force : true , ..._qLog ( ) } )
422438 . then ( ( ) => {
423439 cy . wait ( 2000 , _qLog ( ) ) ;
424440 return incidentsPage . elements . incidentsTable ( ) . scrollIntoView ( ) . should ( 'exist' ) ;
425441 } ) ;
426442 } ,
427443
428- deselectIncidentByBar : ( ) => {
444+ deselectIncidentByBar : ( index = 0 ) => {
429445 if ( ! _quietSearch ) cy . log ( 'incidentsPage.deselectIncidentByBar' ) ;
430446 return incidentsPage . elements
431- . incidentsChartBarsVisiblePaths ( )
432- . then ( ( $paths ) => {
433- if ( $paths . length === 0 ) {
434- throw new Error ( 'No paths found in incidents chart' ) ;
435- }
436- return cy . wrap ( $paths . eq ( 0 ) , _qLog ( ) ) . click ( { force : true , ..._qLog ( ) } ) ;
437- } )
447+ . incidentsChartBarsGroups ( )
448+ . eq ( index )
449+ . find ( 'path[role="presentation"]' )
450+ . filter ( _isVisiblePath )
451+ . first ( )
452+ . click ( { force : true , ..._qLog ( ) } )
438453 . then ( ( ) => {
439454 return incidentsPage . elements . incidentsTable ( ) . should ( 'not.exist' ) ;
440455 } ) ;
@@ -653,8 +668,10 @@ export const incidentsPage = {
653668
654669 prepareIncidentsPageForSearch : ( ) => {
655670 if ( ! _quietSearch ) cy . log ( 'incidentsPage.prepareIncidentsPageForSearch: Setting up page...' ) ;
656- // Force a hard page reload to release browser DOM memory from previous search iterations.
657- cy . reload ( { log : false } ) ;
671+ // Use SPA navigation instead of cy.reload() — the Incidents component is a
672+ // dynamic plugin chunk, and cy.reload() causes the Console to re-resolve all
673+ // plugins from scratch, which silently fails in headless CI (blank page).
674+ // OOM is handled by _quietSearch suppressing DOM snapshots, not by reload.
658675 incidentsPage . goTo ( ) ;
659676 incidentsPage . setDays ( incidentsPage . SEARCH_CONFIG . DEFAULT_DAYS ) ;
660677 incidentsPage . elements . incidentsChartContainer ( ) . should ( 'be.visible' ) ;
@@ -821,7 +838,7 @@ export const incidentsPage = {
821838 if ( found ) {
822839 return cy . wrap ( true , _qLog ( ) ) ;
823840 }
824- incidentsPage . deselectIncidentByBar ( ) ;
841+ incidentsPage . deselectIncidentByBar ( currentIndex ) ;
825842 cy . wait ( 500 , _qLog ( ) ) ;
826843 return searchNextIncidentBar ( currentIndex + 1 ) ;
827844 } ) ;
@@ -859,16 +876,18 @@ export const incidentsPage = {
859876
860877 incidentsPage . prepareIncidentsPageForSearch ( ) ;
861878
862- return incidentsPage . elements
863- . incidentsChartBarsVisiblePaths ( )
864- . then ( ( $paths ) => {
865- const totalPaths = $paths . length ;
866- if ( totalPaths === 0 ) {
867- cy . log ( 'No visible incident bar paths found in chart' ) ;
879+ // Check for bar groups without asserting existence — an empty chart is
880+ // valid (e.g. when mocking empty incidents or before detection fires).
881+ return cy
882+ . get ( 'body' , _qLog ( ) )
883+ . then ( ( $body ) => {
884+ const totalIncidents = $body . find ( _BAR_GROUP_SELECTOR ) . length ;
885+ if ( totalIncidents === 0 ) {
886+ if ( ! _quietSearch ) cy . log ( 'No incident bar groups found in chart' ) ;
868887 return cy . wrap ( false , { log : false } ) ;
869888 }
870889
871- return incidentsPage . traverseAllIncidentsBars ( alertName , totalPaths ) ;
890+ return incidentsPage . traverseAllIncidentsBars ( alertName , totalIncidents ) ;
872891 } )
873892 . then ( ( found : boolean ) => {
874893 if ( found ) {
0 commit comments