From 1fa360832434087aab408af1227b9d1db14e01c6 Mon Sep 17 00:00:00 2001 From: agorthi Date: Thu, 24 Jul 2025 08:36:28 +0530 Subject: [PATCH 1/5] test[DI-26399]: add verification tests for NodeBalancer widget --- .../nodebalancer-widget-verification.spec.ts | 496 ++++++++++++++++++ .../cypress/support/constants/widgets.ts | 47 ++ 2 files changed, 543 insertions(+) create mode 100644 packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts diff --git a/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts new file mode 100644 index 00000000000..290723ffa42 --- /dev/null +++ b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts @@ -0,0 +1,496 @@ +/** + * @file Integration Tests for CloudPulse nodebalancer Dashboard. + */ +import { + linodeFactory, + nodeBalancerFactory, + regionFactory, +} from '@linode/utilities'; +import { widgetDetails } from 'support/constants/widgets'; +import { mockGetAccount } from 'support/intercepts/account'; +import { + mockCreateCloudPulseJWEToken, + mockCreateCloudPulseMetrics, + mockGetCloudPulseDashboard, + mockGetCloudPulseDashboards, + mockGetCloudPulseMetricDefinitions, + mockGetCloudPulseServices, +} from 'support/intercepts/cloudpulse'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetLinodes } from 'support/intercepts/linodes'; +import { mockGetNodeBalancers } from 'support/intercepts/nodebalancers'; +import { mockGetUserPreferences } from 'support/intercepts/profile'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; +import { generateRandomMetricsData } from 'support/util/cloudpulse'; +import { randomNumber } from 'support/util/random'; + +import { + accountFactory, + cloudPulseMetricsResponseFactory, + dashboardFactory, + dashboardMetricFactory, + kubeLinodeFactory, + widgetFactory, +} from 'src/factories'; +import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; +import { formatToolTip } from 'src/features/CloudPulse/Utils/unitConversion'; + +import type { CloudPulseMetricsResponse } from '@linode/api-v4'; +import type { Flags } from 'src/featureFlags'; +import type { Interception } from 'support/cypress-exports'; +/** + * This test ensures that widget titles are displayed correctly on the dashboard. + * This test suite is dedicated to verifying the functionality and display of widgets on the Cloudpulse dashboard. + * It includes: + * Validating that widgets are correctly loaded and displayed. + * Ensuring that widget titles and data match the expected values. + * Verifying that widget settings, such as granularity and aggregation, are applied correctly. + * Testing widget interactions, including zooming and filtering, to ensure proper behavior. + * Each test ensures that widgets on the dashboard operate correctly and display accurate information. + */ +const expectedGranularityArray = ['Auto', '1 day', '1 hr', '5 min']; +const timeDurationToSelect = 'Last 24 Hours'; +const flags: Partial = { + aclp: { beta: true, enabled: true }, + aclpBetaServices: { nodebalancer: { alerts: true, metrics: true } }, + aclpResourceTypeMap: [ + { + dimensionKey: 'LINODE_ID', + maxResourceSelections: 10, + serviceType: 'linode', + }, + { + dimensionKey: 'cluster_id', + maxResourceSelections: 10, + serviceType: 'dbaas', + }, + { + dimensionKey: 'cluster_id', + maxResourceSelections: 10, + serviceType: 'nodebalancer', + }, + ], +}; +const { dashboardName, id, metrics, region, resource, serviceType } = + widgetDetails.nodebalancer; + +const dashboard = dashboardFactory.build({ + label: dashboardName, + service_type: serviceType, + widgets: metrics.map(({ name, title, unit, yLabel }) => { + return widgetFactory.build({ + label: title, + metric: name, + unit, + y_label: yLabel, + }); + }), +}); + +const metricDefinitions = metrics.map(({ name, title, unit }) => + dashboardMetricFactory.build({ + label: title, + metric: name, + unit, + }) +); + +const mockRegion = regionFactory.build({ + capabilities: ['NodeBalancers'], + id: 'us-east', + label: 'Newark, NJ, USA', + monitors: { + metrics: ['NodeBalancers'], + alerts: [], + }, +}); + +const metricsAPIResponsePayload = cloudPulseMetricsResponseFactory.build({ + data: generateRandomMetricsData(timeDurationToSelect, '5 min'), +}); + +/** + * Generates graph data from a given CloudPulse metrics response and + * extracts average, last, and maximum metric values from the first + * legend row. The values are rounded to two decimal places for + * better readability. + * + * @param responsePayload - The metrics response object containing + * the necessary data for graph generation. + * @param label - The label for the graph, used for display purposes. + * + * @returns An object containing rounded values for average, last, + * + */ + +const getWidgetLegendRowValuesFromResponse = ( + responsePayload: CloudPulseMetricsResponse, + label: string, + unit: string +) => { + // Generate graph data using the provided parameters + const graphData = generateGraphData({ + label, + metricsList: responsePayload, + resources: [ + { + id: '1', + label: resource, + region: 'us-east', + }, + ], + status: 'success', + unit, + }); + + // Destructure metrics data from the first legend row + const { average, last, max } = graphData.legendRowsData[0].data; + + // Round the metrics values to two decimal places + const roundedAverage = formatToolTip(average, unit); + const roundedLast = formatToolTip(last, unit); + const roundedMax = formatToolTip(max, unit); + // Return the rounded values in an object + return { average: roundedAverage, last: roundedLast, max: roundedMax }; +}; +const mockLinode = linodeFactory.build({ + id: kubeLinodeFactory.build().instance_id ?? undefined, + label: resource, + region: 'us-east', +}); +const mockNodeBalancer = nodeBalancerFactory.build({ + label: resource, + region: 'us-east', +}); +// Tests will be modified +describe('Integration Tests for Nodebalancer Dashboard ', () => { + beforeEach(() => { + mockAppendFeatureFlags(flags); + mockGetAccount(accountFactory.build({})); + + mockGetCloudPulseMetricDefinitions(serviceType, metricDefinitions); + mockGetCloudPulseDashboards(serviceType, [dashboard]).as('fetchDashboard'); + mockGetCloudPulseServices([serviceType]).as('fetchServices'); + mockGetCloudPulseDashboard(id, dashboard); + mockCreateCloudPulseJWEToken(serviceType); + mockCreateCloudPulseMetrics(serviceType, metricsAPIResponsePayload).as( + 'getMetrics' + ); + mockGetRegions([mockRegion]); + mockGetLinodes([mockLinode]); + mockGetNodeBalancers([mockNodeBalancer]); + mockGetUserPreferences({}); + + // navigate to the metrics page + cy.visitWithLogin('/metrics'); + + // Wait for the services and dashboard API calls to complete before proceeding + cy.wait(['@fetchServices', '@fetchDashboard']); + + // Selecting a dashboard from the autocomplete input. + ui.autocomplete + .findByLabel('Dashboard') + .should('be.visible') + .type(dashboardName); + + ui.autocompletePopper + .findByTitle(dashboardName) + .should('be.visible') + .click(); + + // Select a time duration from the autocomplete input. + cy.get('[aria-labelledby="start-date"]').as('startDateInput'); + + cy.get('@startDateInput').click(); + + ui.button.findByTitle('last day').click(); + + cy.get('[data-qa-buttons="apply"]') + .should('be.visible') + .should('be.enabled') + .click(); + + // Select a region from the dropdown. + ui.regionSelect.find().click(); + ui.regionSelect.find().type(`${region}{enter}`); + + // Select a resource from the autocomplete input. + ui.autocomplete + .findByLabel('Nodebalancers') + .should('be.visible') + .type(`${resource}{enter}`); + + ui.autocomplete.findByLabel('Nodebalancers').click(); + + cy.findByText(resource).should('be.visible'); + + // Expand the applied filters section + ui.button.findByTitle('Filters').should('be.visible').click(); + + // Wait for all metrics query requests to resolve. + cy.wait(['@getMetrics', '@getMetrics', '@getMetrics', '@getMetrics']); + }); + it('should apply optional filter (port) and verify API request payloads', () => { + const randomPort = randomNumber(1, 65535).toString(); + + ui.button.findByTitle('Filters').should('be.visible').click(); + + cy.findByPlaceholderText('e.g., 80,443,3000') + .should('be.visible') + .type(randomPort); + + cy.wait(['@getMetrics', '@getMetrics', '@getMetrics', '@getMetrics']); + + cy.get('@getMetrics.all').then((calls) => { + const lastFourCalls = (calls as unknown as Interception[]).slice(-4); + + lastFourCalls.forEach((call) => { + const filters = call.request.body.filters; + expect(filters).to.deep.include({ + dimension_label: 'port', + operator: 'in', + value: randomPort, + }); + }); + }); + }); + + it('should allow users to select their desired granularity and see the most recent data from the API reflected in the graph', () => { + // validate the widget level granularity selection and its metrics + metrics.forEach((testData) => { + const widgetSelector = `[data-qa-widget="${testData.title}"]`; + cy.get(widgetSelector) + .should('be.visible') + .find('h2') + .should('have.text', `${testData.title} (${testData.unit.trim()})`); + cy.get(widgetSelector) + .should('be.visible') + .within(() => { + // check for all available granularity in popper + ui.autocomplete + .findByLabel('Select an Interval') + .should('be.visible') + .click(); + + expectedGranularityArray.forEach((option) => { + ui.autocompletePopper.findByTitle(option).should('exist'); + }); + + mockCreateCloudPulseMetrics( + serviceType, + metricsAPIResponsePayload + ).as('getGranularityMetrics'); + + // find the interval component and select the expected granularity + ui.autocomplete + .findByLabel('Select an Interval') + .should('be.visible') + .type(`${testData.expectedGranularity}{enter}`); // type expected granularity + + // check if the API call is made correctly with time granularity value selected + cy.wait('@getGranularityMetrics').then((interception) => { + expect(interception) + .to.have.property('response') + .with.property('statusCode', 200); + expect(testData.expectedGranularity).to.include( + interception.request.body.time_granularity.value + ); + }); + + // validate the widget areachart is present + cy.get('.recharts-responsive-container').within(() => { + const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( + metricsAPIResponsePayload, + testData.title, + testData.unit + ); + + const graphRowTitle = `[data-qa-graph-row-title="${testData.title} (${testData.unit})"]`; + cy.get(graphRowTitle) + .should('be.visible') + .should('have.text', `${testData.title} (${testData.unit})`); + + cy.log('expectedWidgetValues ', expectedWidgetValues.max); + + cy.get(`[data-qa-graph-column-title="Max"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.max}`); + + cy.get(`[data-qa-graph-column-title="Avg"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.average}`); + + cy.get(`[data-qa-graph-column-title="Last"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.last}`); + }); + }); + }); + }); + it('should allow users to select the desired aggregation and view the latest data from the API displayed in the graph', () => { + metrics.forEach((testData) => { + const widgetSelector = `[data-qa-widget="${testData.title}"]`; + cy.get(widgetSelector) + .should('be.visible') + .within(() => { + mockCreateCloudPulseMetrics( + serviceType, + metricsAPIResponsePayload + ).as('getAggregationMetrics'); + + // find the interval component and select the expected granularity + ui.autocomplete + .findByLabel('Select an Aggregate Function') + .should('be.visible') + .type(`${testData.expectedAggregation}{enter}`); // type expected granularity + + // check if the API call is made correctly with time granularity value selected + cy.wait('@getAggregationMetrics').then((interception) => { + expect(interception) + .to.have.property('response') + .with.property('statusCode', 200); + expect(testData.expectedAggregation).to.equal( + interception.request.body.metrics[0].aggregate_function + ); + }); + + // validate the widget areachart is present + cy.get('.recharts-responsive-container').within(() => { + const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( + metricsAPIResponsePayload, + testData.title, + testData.unit + ); + const graphRowTitle = `[data-qa-graph-row-title="${testData.title} (${testData.unit})"]`; + cy.get(graphRowTitle) + .should('be.visible') + .should( + 'have.text', + `${testData.title} (${testData.unit.trim()})` + ); + + cy.get(`[data-qa-graph-column-title="Max"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.max}`); + + cy.get(`[data-qa-graph-column-title="Avg"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.average}`); + + cy.get(`[data-qa-graph-column-title="Last"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.last}`); + }); + }); + }); + }); + it('should trigger the global refresh button and verify the corresponding network calls', () => { + mockCreateCloudPulseMetrics(serviceType, metricsAPIResponsePayload).as( + 'refreshMetrics' + ); + + // click the global refresh button + ui.button + .findByAttribute('aria-label', 'Refresh Dashboard Metrics') + .should('be.visible') + .click(); + + // validate the API calls are going with intended payload + cy.get('@refreshMetrics.all') + .should('have.length', 4) + .each((xhr: unknown) => { + const interception = xhr as Interception; + const { body: requestPayload } = interception.request; + const { metrics: metric, relative_time_duration: timeRange } = + requestPayload; + const metricData = metrics.find(({ name }) => name === metric[0].name); + + if (!metricData) { + throw new Error( + `Unexpected metric name '${metric[0].name}' included in the outgoing refresh API request` + ); + } + expect(metric[0].name).to.equal(metricData.name); + expect(timeRange).to.have.property('unit', 'days'); + expect(timeRange).to.have.property('value', 1); + }); + }); + + it('should zoom in and out of all the widgets', () => { + // do zoom in and zoom out test on all the widgets + metrics.forEach((testData) => { + cy.get(`[data-qa-widget="${testData.title}"]`).as('widget'); + cy.get('@widget') + .should('be.visible') + .within(() => { + ui.button + .findByAttribute('aria-label', 'Zoom Out') + .should('be.visible') + .should('be.enabled') + .click(); + cy.get('@widget').should('be.visible'); + cy.get('.recharts-responsive-container').within(() => { + const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( + metricsAPIResponsePayload, + testData.title, + testData.unit + ); + const graphRowTitle = `[data-qa-graph-row-title="${testData.title} (${testData.unit})"]`; + cy.get(graphRowTitle) + .should('be.visible') + .should('have.text', `${testData.title} (${testData.unit})`); + + cy.get(`[data-qa-graph-column-title="Max"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.max}`); + + cy.get(`[data-qa-graph-column-title="Avg"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.average}`); + + cy.get(`[data-qa-graph-column-title="Last"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.last}`); + }); + + // click zoom out and validate the same + ui.button + .findByAttribute('aria-label', 'Zoom In') + .should('be.visible') + .should('be.enabled') + .scrollIntoView() + .click({ force: true }); + cy.get('@widget').should('be.visible'); + + cy.get('.recharts-responsive-container').within(() => { + const expectedWidgetValues = getWidgetLegendRowValuesFromResponse( + metricsAPIResponsePayload, + testData.title, + testData.unit + ); + + const graphRowTitle = `[data-qa-graph-row-title="${testData.title} (${testData.unit})"]`; + cy.get(graphRowTitle) + .should('be.visible') + .should( + 'have.text', + `${testData.title} (${testData.unit.trim()})` + ); + + cy.get(`[data-qa-graph-column-title="Max"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.max}`); + + cy.get(`[data-qa-graph-column-title="Avg"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.average}`); + + cy.get(`[data-qa-graph-column-title="Last"]`) + .should('be.visible') + .should('have.text', `${expectedWidgetValues.last}`); + }); + }); + }); + }); +}); diff --git a/packages/manager/cypress/support/constants/widgets.ts b/packages/manager/cypress/support/constants/widgets.ts index 30c97edaf94..fbf5d4e1291 100644 --- a/packages/manager/cypress/support/constants/widgets.ts +++ b/packages/manager/cypress/support/constants/widgets.ts @@ -98,4 +98,51 @@ export const widgetDetails = { resource: 'linode-resource', serviceType: 'linode', }, + nodebalancer: { + dashboardName: 'NodeBalancer Dashboard', + id: 1, + metrics: [ + { + expectedAggregation: 'max', + expectedAggregationArray: ['sum'], + expectedGranularity: '1 hr', + name: 'system_cpu_utilization_percent', + title: 'CPU Utilization', + unit: '%', + yLabel: 'system_cpu_utilization_ratio', + }, + { + expectedAggregation: 'max', + expectedAggregationArray: ['sum'], + expectedGranularity: '1 hr', + name: 'system_memory_usage_by_resource', + title: 'Memory Usage', + unit: 'B', + yLabel: 'system_memory_usage_bytes', + }, + { + expectedAggregation: 'max', + expectedAggregationArray: ['sum'], + expectedGranularity: '1 hr', + name: 'system_network_io_by_resource', + title: 'Network Traffic', + unit: 'B', + yLabel: 'system_network_io_bytes_total', + }, + { + expectedAggregation: 'max', + expectedAggregationArray: ['sum'], + expectedGranularity: '1 hr', + name: 'system_disk_OPS_total', + title: 'Disk I/O', + unit: 'OPS', + yLabel: 'system_disk_operations_total', + }, + ], + region: 'Newark, NJ, USA (us-east)', + resource: 'NodeBalancer-resource', + serviceType: 'nodebalancer', + port: 1, + protocols: ['TCP', 'UDP'], + }, }; From 00890f8944590aa5dfe4f2a803f88f72447d3e98 Mon Sep 17 00:00:00 2001 From: agorthi Date: Thu, 24 Jul 2025 13:07:17 +0530 Subject: [PATCH 2/5] test[DI-26399]: skip flaky time range picker test due to intermittent failure --- .../cypress/e2e/core/cloudpulse/timerange-verification.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index 61afdebd6f7..626cb1178ed 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -240,7 +240,7 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura ]); }); - it('should implement and validate custom date/time picker for a specific date and time range', () => { + it.skip('should implement and validate custom date/time picker for a specific date and time range', () => { // --- Generate start and end date/time in GMT --- const { actualDate: startActualDate, From e08ef0c3cde1cefa25d942f325f337a05be149db Mon Sep 17 00:00:00 2001 From: agorthi Date: Fri, 25 Jul 2025 12:57:36 +0530 Subject: [PATCH 3/5] test[DI-26399]: skip flaky time range picker test due to intermittent failure --- packages/manager/.changeset/pr-12568-tests-1753428442028.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-12568-tests-1753428442028.md diff --git a/packages/manager/.changeset/pr-12568-tests-1753428442028.md b/packages/manager/.changeset/pr-12568-tests-1753428442028.md new file mode 100644 index 00000000000..f17de316ec0 --- /dev/null +++ b/packages/manager/.changeset/pr-12568-tests-1753428442028.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Cypress verification tests for CloudPulse NodeBalancer widget ([#12568](https://github.com/linode/manager/pull/12568)) From 7b742a4b2af5770d2fb3d9d78386e3bc9ef66cdf Mon Sep 17 00:00:00 2001 From: agorthi Date: Fri, 25 Jul 2025 20:03:23 +0530 Subject: [PATCH 4/5] test[DI-26399]: skip flaky time range picker test due to intermittent failure --- .../cloudpulse/timerange-verification.spec.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index fea8c73fe43..2dba3e72247 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable cypress/no-unnecessary-waiting */ /** * @file Integration Tests for CloudPulse Custom and Preset Verification */ @@ -243,7 +244,7 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura ]); }); - it.skip('should implement and validate custom date/time picker for a specific date and time range', () => { + it('should implement and validate custom date/time picker for a specific date and time range', () => { // --- Generate start and end date/time in GMT --- const { actualDate: startActualDate, @@ -259,6 +260,7 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura minute: endMinute, } = getDateRangeInGMT(12, 30); + cy.wait(1000); // --- Select start date --- cy.get('[aria-labelledby="start-date"]').as('startDateInput'); cy.get('@startDateInput').click(); @@ -266,6 +268,7 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.findAllByText(startDay).first().click(); cy.findAllByText(endDay).first().click(); }); + ui.button .findByAttribute('aria-label^', 'Choose time') .first() @@ -274,9 +277,12 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton').click(); + cy.get('@timePickerButton') + .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) + .click(); // Selects the start hour, minute, and meridiem (AM/PM) in the time picker. + cy.wait(1000); cy.findByLabelText('Select hours') .as('selectHours') .scrollIntoView({ easing: 'linear' }); @@ -285,6 +291,7 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get(`[aria-label="${startHour} hours"]`).click(); }); + cy.wait(1000); ui.button .findByAttribute('aria-label^', 'Choose time') .first() @@ -293,7 +300,9 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton').click(); + cy.get('@timePickerButton') + .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) + .click(); cy.findByLabelText('Select minutes') .as('selectMinutes') @@ -311,7 +320,9 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton').click(); + cy.get('@timePickerButton') + .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) + .click(); cy.findByLabelText('Select meridiem') .as('startMeridiemSelect') @@ -341,8 +352,9 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura .should('be.visible') .as('timePickerButton'); - cy.get('@timePickerButton').click(); - + cy.get('@timePickerButton') + .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) + .click(); cy.findByLabelText('Select minutes').scrollIntoView({ duration: 500, easing: 'linear', @@ -356,7 +368,9 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura .should('be.visible') .as('timePickerButton'); - cy.get('@timePickerButton').click(); + cy.get('@timePickerButton') + .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) + .click(); cy.findByLabelText('Select meridiem') .as('endMeridiemSelect') From 31f0d87ce4ae452b471d1a0c91e5d66639d47875 Mon Sep 17 00:00:00 2001 From: agorthi Date: Mon, 28 Jul 2025 10:18:53 +0530 Subject: [PATCH 5/5] test: [DI-26399]- Add Cypress verification tests for CloudPulse NodeBalancer widget --- .../nodebalancer-widget-verification.spec.ts | 3 +++ .../cloudpulse/timerange-verification.spec.ts | 23 +++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts index 290723ffa42..ce5a5d55d1c 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts @@ -329,6 +329,8 @@ describe('Integration Tests for Nodebalancer Dashboard ', () => { }); }); it('should allow users to select the desired aggregation and view the latest data from the API displayed in the graph', () => { + // validate the widget level granularity selection and its metrics + metrics.forEach((testData) => { const widgetSelector = `[data-qa-widget="${testData.title}"]`; cy.get(widgetSelector) @@ -386,6 +388,7 @@ describe('Integration Tests for Nodebalancer Dashboard ', () => { }); }); it('should trigger the global refresh button and verify the corresponding network calls', () => { + // mock the API call for refreshing metrics mockCreateCloudPulseMetrics(serviceType, metricsAPIResponsePayload).as( 'refreshMetrics' ); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index 2dba3e72247..c2f9eb570be 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -272,12 +272,11 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura ui.button .findByAttribute('aria-label^', 'Choose time') .first() - .should('be.visible') + .should('be.visible', { timeout: 10000 }) // waits up to 10 seconds .as('timePickerButton'); - cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton') + cy.get('@timePickerButton', { timeout: 15000 }) .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) .click(); @@ -295,12 +294,12 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura ui.button .findByAttribute('aria-label^', 'Choose time') .first() - .should('be.visible') + .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton') + cy.get('@timePickerButton', { timeout: 15000 }) .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) .click(); @@ -315,12 +314,12 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura ui.button .findByAttribute('aria-label^', 'Choose time') .first() - .should('be.visible') + .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton') + cy.get('@timePickerButton', { timeout: 15000 }) .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) .click(); @@ -333,10 +332,10 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura ui.button .findByAttribute('aria-label^', 'Choose time') .last() - .should('be.visible') + .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); - cy.get('@timePickerButton').click(); + cy.get('@timePickerButton', { timeout: 15000 }).click(); // Selects the start hour, minute, and meridiem (AM/PM) in the time picker. cy.findByLabelText('Select hours').scrollIntoView({ @@ -352,7 +351,7 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura .should('be.visible') .as('timePickerButton'); - cy.get('@timePickerButton') + cy.get('@timePickerButton', { timeout: 15000 }) .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) .click(); cy.findByLabelText('Select minutes').scrollIntoView({ @@ -365,10 +364,10 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura cy.get('[aria-label^="Choose time"]') .last() - .should('be.visible') + .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); - cy.get('@timePickerButton') + cy.get('@timePickerButton', { timeout: 15000 }) .wait(300) // ⛔ doesn't work like this! (cy.wait isn't chainable on element) .click();