Skip to content

Commit d0bed32

Browse files
committed
test: Randomize alert name and minor fixes
00.coo_incidents_e2e - the created alert name is different for every test - improved test name and logging 01.incidents.cy.ts - improved test names and logging - merged test 5 and 6 together - added toggling the button in test 1 commands.ts - command for logging when using cy.waitUntil incident-page.ts - more stable lookup for the alert bars
1 parent a4b8209 commit d0bed32

6 files changed

Lines changed: 127 additions & 34 deletions

File tree

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
The test verifies the whole lifecycle of the Incident feature, without any external dependencies.
3+
The run time can be 15 - 20 minutes. (Waiting untill the incident detection captures the new alert)
34
*/
45
import { commonPages } from '../../views/common';
56
import { incidentsPage } from '../../views/incidents-page';
@@ -20,40 +21,50 @@ const MP = {
2021
operatorName: 'Cluster Monitoring Operator',
2122
};
2223

23-
describe('Incidents', () => {
24+
describe('BVT: Incidents - e2e', () => {
25+
let currentAlertName: string;
26+
2427
before(() => {
2528
cy.afterBlockCOO(MCP, MP); // Following cypher best practices, the cleanup is done before the test block
2629
cy.beforeBlockCOO(MCP, MP);
27-
cy.createKubePodCrashLoopingAlert();
30+
31+
cy.cleanupIncidentPrometheusRules();
32+
33+
// Create the alert and capture the random name
34+
cy.createKubePodCrashLoopingAlert().then((alertName) => {
35+
currentAlertName = alertName;
36+
cy.log(`Test will look for alert: ${currentAlertName}`);
37+
});
2838
});
2939

3040
after(() => {
3141
cy.afterBlockCOO(MCP, MP); // For compatibility with other tests
3242
});
3343

34-
it('Admin Perspective - Incidents tab renders and responds to interactions', () => {
35-
cy.log('1.1 Navigate to Alerting → Incidents');
36-
incidentsPage.goTo();
37-
commonPages.titleShouldHaveText('Incidents');
38-
});
39-
40-
it('Incident with KubePodCrashLooping alert is present in the alerts table', () => {
44+
it('1. Admin perspective - Incidents page - Incident with custom alert lifecycle', () => {
45+
cy.log('1.1 Navigate to Incidents page and clear filters');
4146
incidentsPage.goTo();
4247
commonPages.titleShouldHaveText('Incidents');
4348
incidentsPage.clearAllFilters();
4449

4550
const intervalMs = 60_000;
46-
const maxMinutes = 20;
51+
const maxMinutes = 30;
4752

48-
cy.waitUntil(() => incidentsPage.findIncidentWithAlert('KubePodCrashLooping'), {
49-
interval: intervalMs,
50-
timeout: maxMinutes * intervalMs
51-
});
53+
cy.log('1.2 Wait for incident with custom alert to appear');
54+
cy.waitUntilWithCustomTimeout(
55+
() => incidentsPage.findIncidentWithAlert(currentAlertName),
56+
{
57+
interval: intervalMs,
58+
timeout: maxMinutes * intervalMs,
59+
timeoutMessage: `Custom timeout: Incident with alert "${currentAlertName}" did not appear within ${maxMinutes} minutes.`
60+
}
61+
);
5262

63+
cy.log('1.3 Verify custom alert appears in alerts table');
5364
incidentsPage
5465
.elements
5566
.alertsTable()
56-
.contains('KubePodCrashLooping')
67+
.contains(currentAlertName)
5768
.should('exist');
5869
});
5970
});

web/cypress/e2e/incidents/01.incidents.cy.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const NAMESPACE = 'openshift-monitoring';
3131
const SEVERITY = 'Critical';
3232
const ALERT_DESC = 'This is an alert meant to ensure that the entire alerting pipeline is functional. This alert is always firing, therefore it should always be firing in Alertmanager and always fire against a receiver. There are integrations with various notification mechanisms that send a notification when this alert is not firing. For example the "DeadMansSnitch" integration in PagerDuty.'
3333
const ALERT_SUMMARY = 'An alert that should always be firing to certify that Alertmanager is working properly.'
34-
describe('Incidents', () => {
34+
describe('BVT: Incidents - UI', () => {
3535
before(() => {
3636
cy.afterBlockCOO(MCP, MP); // Following cypher best practices, the cleanup is done before the test block
3737
cy.beforeBlockCOO(MCP, MP);
@@ -49,37 +49,53 @@ describe('Incidents', () => {
4949
commonPages.titleShouldHaveText('Incidents');
5050
});
5151

52-
it('renders toolbar and toggle charts button', () => {
52+
it('1. Admin perspective - Incidents page - Toolbar and charts toggle functionality', () => {
53+
cy.log('1.1 Verify toolbar and toggle charts button');
5354
incidentsPage.elements.toolbar().should('be.visible');
5455
incidentsPage.elements.toggleChartsButton().should('be.visible');
56+
incidentsPage.elements.toggleChartsButton().click();
57+
58+
cy.log('1.2 Verify charts are hidden after toggle');
59+
incidentsPage.elements.incidentsChartTitle().should('not.exist');
60+
incidentsPage.elements.alertsChartTitle().should('not.exist');
61+
incidentsPage.elements.toggleChartsButton().click();
5562
});
5663

57-
it('sets days filter to 3 days and updates the URL', () => {
64+
it('2. Admin perspective - Incidents page - Days filter functionality', () => {
65+
cy.log('2.1 Set days filter to 3 days');
5866
incidentsPage.setDays('3 days');
67+
68+
cy.log('2.2 Verify filter and URL update');
5969
incidentsPage.elements.daysSelect().should('contain.text', '3 days');
6070
cy.url().should('match', /[?&]days=3\+days/);
6171
});
6272

63-
it('toggles Critical filter and updates the URL', () => {
73+
it('3. Admin perspective - Incidents page - Critical filter functionality', () => {
74+
cy.log('3.1 Clear filters and toggle Critical filter');
6475
incidentsPage.clearAllFilters();
6576
incidentsPage.toggleFilter('Critical');
77+
78+
cy.log('3.2 Verify URL is updated with incident filters');
6679
cy.url().should('match', /incidentFilters=.*Critical/);
6780
});
6881

69-
it('shows charts and alerts empty state initially', () => {
82+
it('4. Admin perspective - Incidents page - Charts and alerts empty state', () => {
83+
cy.log('4.1 Verify chart titles are visible');
7084
incidentsPage.elements.incidentsChartTitle().should('be.visible');
7185
incidentsPage.elements.alertsChartTitle().should('be.visible');
86+
87+
cy.log('4.2 Verify alerts chart shows empty state');
7288
incidentsPage.elements.alertsChartEmptyState().should('exist');
7389
});
7490

75-
it('selecting an incident via chart shows alerts and adds groupId to URL', () => {
91+
it('5. Admin perspective - Incidents page - Incident selection and alert details', () => {
92+
cy.log('5.1 Select incident and verify alert details');
7693
incidentsPage.clearAllFilters();
7794
incidentsPage.selectIncidentByBarIndex(0);
7895
cy.url().should('match', /[?&]groupId=/);
7996
incidentsPage.elements.alertsChartSvg().find('path').should('exist');
80-
});
8197

82-
it('shows alerts table and allows expanding a row', () => {
98+
cy.log('5.2 Verify alerts table and expand first row');
8399
incidentsPage.elements.alertsTable().should('exist');
84100
incidentsPage.expandRow(0);
85101
});

web/cypress/fixtures/incidents/prometheus_rule_pod_crash_loop.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ spec:
1212
groups:
1313
- name: kubernetes-apps
1414
rules:
15-
- alert: KubePodCrashLooping
15+
- alert: {{ALERT_NAME}}
1616
annotations:
1717
description: 'Pod {{ $labels.namespace }}/{{ $labels.pod }} ({{ $labels.container
1818
}}) is in waiting state (reason: "CrashLoopBackOff").'

web/cypress/support/commands.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ declare global {
5151
adminCLI(command: string, options?);
5252
login(provider?: string, username?: string, password?: string): Chainable<Element>;
5353
executeAndDelete(command: string);
54+
waitUntilWithCustomTimeout(
55+
fn: () => any,
56+
options: { interval: number; timeout: number; timeoutMessage: string }
57+
): Cypress.Chainable<any>;
5458
}
5559
}
5660

@@ -630,21 +634,78 @@ Cypress.Commands.add('afterBlockCOO', (MCP: { namespace: string, operatorName: s
630634
// Apply incident fixture manifests to the cluster
631635
Cypress.Commands.add('createKubePodCrashLoopingAlert', () => {
632636
const kubeconfigPath = Cypress.env('KUBECONFIG_PATH');
633-
cy.exec(
634-
`oc apply -f ./cypress/fixtures/incidents/prometheus_rule_pod_crash_loop.yaml --kubeconfig ${kubeconfigPath}`,
635-
);
637+
638+
// Generate a random alert name for this test run
639+
const randomAlertName = `CustomPodCrashLooping_${Math.random().toString(36).substring(2, 15)}`;
640+
641+
// Store the alert name globally so tests can access it
642+
Cypress.env('CURRENT_ALERT_NAME', randomAlertName);
643+
644+
cy.log(`Generated random alert name: ${randomAlertName}`);
645+
646+
// Read the template and replace the placeholder
647+
cy.readFile('./cypress/fixtures/incidents/prometheus_rule_pod_crash_loop.yaml').then((template) => {
648+
const yamlContent = template.replace(/\{\{ALERT_NAME\}\}/g, randomAlertName);
649+
650+
// Write the modified YAML to a temporary file
651+
cy.writeFile('./cypress/fixtures/incidents/temp_prometheus_rule.yaml', yamlContent).then(() => {
652+
// Apply the modified YAML
653+
cy.exec(
654+
`oc apply -f ./cypress/fixtures/incidents/temp_prometheus_rule.yaml --kubeconfig ${kubeconfigPath}`,
655+
);
656+
657+
// Clean up temporary file
658+
cy.exec('rm ./cypress/fixtures/incidents/temp_prometheus_rule.yaml');
659+
});
660+
});
661+
636662
cy.exec(
637663
`oc apply -f ./cypress/fixtures/incidents/pod_crash_loop.yaml --kubeconfig ${kubeconfigPath}`,
638664
);
665+
666+
// Return the alert name for the test to use
667+
return cy.wrap(randomAlertName);
639668
});
640669

641670
// Clean up incident fixture manifests from the cluster
642-
Cypress.Commands.add('cleanupIncidentsFixtures', () => {
671+
Cypress.Commands.add('cleanupIncidentPrometheusRules', () => {
643672
const kubeconfigPath = Cypress.env('KUBECONFIG_PATH');
644-
cy.executeAndDelete(
645-
`oc delete -f ./cypress/fixtures/incidents/pod_crash_loop.yaml --ignore-not-found=true --kubeconfig ${kubeconfigPath}`,
673+
674+
// Delete all PrometheusRules that match our pattern (kubernetes-monitoring-podcrash-rules)
675+
// This ensures cleanup before tests and after tests
676+
cy.exec(
677+
`oc delete prometheusrule kubernetes-monitoring-podcrash-rules -n openshift-monitoring --kubeconfig ${kubeconfigPath} --ignore-not-found=true`,
646678
);
679+
680+
// Clear the environment variable if it exists
681+
if (Cypress.env('CURRENT_ALERT_NAME')) {
682+
Cypress.env('CURRENT_ALERT_NAME', null);
683+
}
684+
647685
cy.executeAndDelete(
648-
`oc delete -f ./cypress/fixtures/incidents/prometheus_rule_pod_crash_loop.yaml --ignore-not-found=true --kubeconfig ${kubeconfigPath}`,
686+
`oc delete -f ./cypress/fixtures/incidents/pod_crash_loop.yaml --ignore-not-found=true --kubeconfig ${kubeconfigPath}`,
649687
);
688+
});
689+
690+
// Custom waitUntil with timeout message
691+
Cypress.Commands.add('waitUntilWithCustomTimeout', (
692+
fn: () => any,
693+
options: { interval: number; timeout: number; timeoutMessage: string }
694+
) => {
695+
const { timeoutMessage, ...waitOptions } = options;
696+
697+
// Set up custom error handling before the waitUntil call
698+
cy.on('fail', (err) => {
699+
if (err.message.includes('Timed out retrying')) {
700+
// Create a new error with the custom message
701+
const customError = new Error(timeoutMessage);
702+
customError.stack = err.stack;
703+
throw customError;
704+
}
705+
// For any other errors, re-throw them unchanged
706+
throw err;
707+
});
708+
709+
// Execute the waitUntil with the original options (without timeoutMessage)
710+
return cy.waitUntil(fn, waitOptions);
650711
});

web/cypress/support/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import './nav';
22
import './selectors';
33
import './commands';
4-
import './alert-injection';
5-
import './prometheus-query-mocks';
64

75
export const checkErrors = () =>
86
cy.window().then((win) => {

web/cypress/views/incidents-page.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,25 @@ export const incidentsPage = {
6161

6262

6363
findIncidentWithAlert: (alertName: string): Cypress.Chainable<boolean> => {
64+
cy.reload();
6465
cy.log(`incidentsPage.findIncidentWithAlert: ${alertName}`);
66+
// Bars are sometimes not loaded immidiately, this refreshes
67+
// the charts and ensures the bars are loaded if present
68+
incidentsPage.setDays('1 day');
69+
6570
return incidentsPage.elements.incidentsChartCard()
6671
.find('path[role="presentation"]')
6772
.then(($bars) => {
6873
const totalBars = $bars.length;
6974
if (totalBars <= 3) { // 3 paths are always present in the legend,
7075
// their parent is g without presentation label opposed ot the proper bars
71-
cy.task('log', 'No bars found in incidents chart');
76+
cy.log('No bars found in incidents chart');
7277
return cy.wrap(false);
7378
}
7479

7580
const tryIndex = (index: number): Cypress.Chainable<boolean> => {
7681
if (index >= totalBars - 3) {
82+
cy.log("Index out of bounds for bars in incidents chart");
7783
return cy.wrap(false);
7884
}
7985

@@ -89,6 +95,7 @@ export const incidentsPage = {
8995
return cy.wrap(true);
9096
}
9197
// Expand a row if present to surface nested details
98+
// Currently expects only one row to be present
9299
incidentsPage.expandRow(0);
93100
return incidentsPage.elements.alertsTable().invoke('text').then((text2) => {
94101
if (String(text2).includes(alertName)) {

0 commit comments

Comments
 (0)