From cd0eff459d9e69bffd9f52f0850816d5ffb94eff Mon Sep 17 00:00:00 2001 From: Shrinath Rao Date: Fri, 4 Sep 2026 04:05:22 +0000 Subject: [PATCH] test(autoe2e): generated E2E tests for #14046 (alert-list-frequency-retries) (#14061) Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com> --- .../pages/alertsPages/alertsPage.js | 112 ++++++++++++++++++ .../Alerts/alerts-priority-tags.spec.js | 105 +++++++++++++++- .../utils/alerts-api-helpers.js | 14 ++- 3 files changed, 229 insertions(+), 2 deletions(-) diff --git a/tests/ui-testing/pages/alertsPages/alertsPage.js b/tests/ui-testing/pages/alertsPages/alertsPage.js index e24368523f..3ed95889b6 100644 --- a/tests/ui-testing/pages/alertsPages/alertsPage.js +++ b/tests/ui-testing/pages/alertsPages/alertsPage.js @@ -310,6 +310,19 @@ export class AlertsPage { alertListTabRealtime: '[data-test="alert-list-tab-realTime"]', alertListFilterChip: '[data-test="alert-list-filter-chip"]', + // Frequency (cadence) cell — auto-generated by OTable as o2-table-cell-{columnId} + frequencyCell: '[data-test="o2-table-cell-frequency"]', + + // Evaluation-history retries cell (AlertEvaluationHistory.vue) + alertDetailsHistoryRetriesCell: '[data-test="alerts-alertevaluationhistory-retries"]', + + // Section tab strip (AlertSectionTabs.vue) — header tab switcher + alertSectionTabs: '[data-test="alert-section-tabs"]', + alertSectionTabAlertList: '[data-test="alert-section-tab-alertList"]', + alertSectionTabAlertDestinations: '[data-test="alert-section-tab-alertDestinations"]', + alertSectionTabAlertTemplates: '[data-test="alert-section-tab-alertTemplates"]', + alertSectionTabAlertLibrary: '[data-test="alert-section-tab-alertLibrary"]', + // v3 AddAlert wizard tabs (OToggleGroupItem with data-test="add-alert-tab-{key}") addAlertTabCondition: '[data-test="add-alert-tab-condition"]', addAlertTabAdvanced: '[data-test="add-alert-tab-advanced"]', @@ -1351,6 +1364,105 @@ export class AlertsPage { } } + /** + * Click an alert-type filter tab (all / scheduled / realTime) and wait until it is + * actually active (data-state="on") before returning, so callers never assert a + * downstream column against a tab whose click silently failed. + * @param {'all' | 'scheduled' | 'realTime'} value + */ + async clickAlertTypeTab(value) { + const tabs = { + all: this.locators.alertListTabAll, + scheduled: this.locators.alertListTabScheduled, + realTime: this.locators.alertListTabRealtime, + }; + const tab = this.page.locator(tabs[value]); + await expect(tab).toBeVisible({ timeout: 10000 }); + await tab.click(); + await expect(tab).toHaveAttribute('data-state', 'on', { timeout: 10000 }); + testLogger.info('Switched alert type tab', { value }); + } + + /** + * Assert the "Check every" (frequency) cell for the given alert row renders `expected` + * (raw cron string, "N Mins", or "--"). Narrows the list to the alert first so the + * row is present even when the list paginates past it. + * @param {string} name + * @param {string} expected + */ + async expectFrequencyCell(name, expected) { + await this.searchAlert(name); + const row = this.page.locator('tbody tr').filter({ hasText: name }).first(); + await expect(row).toBeVisible({ timeout: 15000 }); + const cell = row.locator(this.locators.frequencyCell); + await expect(cell).toBeVisible({ timeout: 10000 }); + await expect(cell).toHaveText(expected, { timeout: 10000 }); + testLogger.info('Frequency cell asserted', { name, expected }); + } + + /** + * Assert the evaluation-history table renders a "Retries" column with at least one + * numeric retries cell (the column lives between Condition and Evaluation Time). + */ + async expectEvaluationHistoryRetriesColumn() { + const headerText = await this.page + .locator(this.locators.alertDetailsHistoryTable) + .locator('thead') + .innerText(); + expect(headerText, 'evaluation history must include a Retries column').toContain('Retries'); + const retriesCell = this.page.locator(this.locators.alertDetailsHistoryRetriesCell).first(); + await expect(retriesCell).toBeVisible({ timeout: 15000 }); + testLogger.info('Evaluation history Retries column and cells present'); + } + + /** + * Assert the newest evaluation's retries cell renders the given integer (0 for a clean + * self-delivery). Newest-first row ordering means `.first()` is the latest run. + * @param {number} value + */ + async expectEvaluationHistoryRetriesValue(value) { + const cell = this.page.locator(this.locators.alertDetailsHistoryRetriesCell).first(); + await expect(cell).toBeVisible({ timeout: 15000 }); + await expect(cell).toHaveText(String(value), { timeout: 10000 }); + testLogger.info('Evaluation history retries value verified', { value }); + } + + /** + * Assert the section tab strip renders the given labels left-to-right in order. + * @param {string[]} labels + */ + async expectSectionTabLabels(labels) { + const strip = this.page.locator(this.locators.alertSectionTabs); + await expect(strip).toBeVisible({ timeout: 10000 }); + const text = await strip.innerText(); + let lastIndex = -1; + for (const label of labels) { + const index = text.indexOf(label); + expect(index, `section tab strip must show "${label}" in order`).toBeGreaterThan(lastIndex); + lastIndex = index; + } + testLogger.info('Section tab labels verified in order', { labels }); + } + + /** + * Assert a specific section tab renders the exact label text (proves the rename landed: + * "Notification Destinations" / "Alert Library" rather than the deleted short forms). + * @param {'alertList' | 'alertDestinations' | 'alertTemplates' | 'alertLibrary'} key + * @param {string} text + */ + async expectSectionTabLabel(key, text) { + const tabs = { + alertList: this.locators.alertSectionTabAlertList, + alertDestinations: this.locators.alertSectionTabAlertDestinations, + alertTemplates: this.locators.alertSectionTabAlertTemplates, + alertLibrary: this.locators.alertSectionTabAlertLibrary, + }; + const tab = this.page.locator(tabs[key]); + await expect(tab).toBeVisible({ timeout: 10000 }); + await expect(tab).toHaveText(text, { timeout: 10000 }); + testLogger.info('Section tab label verified', { key, text }); + } + /** * Open the Advanced-tab template override OSelect dropdown. */ diff --git a/tests/ui-testing/playwright-tests/Alerts/alerts-priority-tags.spec.js b/tests/ui-testing/playwright-tests/Alerts/alerts-priority-tags.spec.js index f825ee92e3..faffd61fd4 100644 --- a/tests/ui-testing/playwright-tests/Alerts/alerts-priority-tags.spec.js +++ b/tests/ui-testing/playwright-tests/Alerts/alerts-priority-tags.spec.js @@ -30,7 +30,10 @@ const testLogger = require('../utils/test-logger.js'); const PageManager = require('../../pages/page-manager.js'); const logData = require('../../fixtures/log.json'); const { getOrgIdentifier } = require('../utils/cloud-auth.js'); -const { findAlertId, getAlert, deleteAlerts, seedAlertFixtures, DEST } = require('../utils/alerts-api-helpers.js'); +const { + findAlertId, getAlert, deleteAlerts, seedAlertFixtures, DEST, + simpleAlert, realtimeAlert, cronAlert, createAlert, waitForAlertOutcome, +} = require('../utils/alerts-api-helpers.js'); const NETWORK_IDLE_TIMEOUT_MS = 30000; const TEST_STREAM = 'e2e_automate'; @@ -176,4 +179,104 @@ test.describe('Alerts — priority, tags & additional variables', { }); testLogger.info('Alert list column set verified'); }); + + // ───────────────────────────────────────────────────────────────────────── + // Frequency ("Check every") cadence cell + column conditional (issue #14038) + // ───────────────────────────────────────────────────────────────────────── + test('frequency cell renders the cadence for cron, minute-interval and real-time alerts', { + tag: ['@alert-list-frequency-retries', '@all', '@alerts', '@P0'], + }, async ({ page }) => { + const suffix = Math.random().toString(36).substring(2, 8); + const cronName = 'auto_freq_cron_' + suffix; + const minsName = 'auto_freq_mins_' + suffix; + const rtName = 'auto_freq_rt_' + suffix; + createdAlertNames.push(cronName, minsName, rtName); + + const cronResp = await createAlert(page, cronAlert(cronName)); + const minsResp = await createAlert(page, simpleAlert(minsName)); + const rtResp = await createAlert(page, realtimeAlert(rtName)); + expect(cronResp.ok(), `cron alert create failed: ${cronResp.status()}`).toBeTruthy(); + expect(minsResp.ok(), `minutes alert create failed: ${minsResp.status()}`).toBeTruthy(); + expect(rtResp.ok(), `realtime alert create failed: ${rtResp.status()}`).toBeTruthy(); + + await page.goto(`${logData.alertUrl}?org_identifier=${getOrgIdentifier()}`); + await page.waitForLoadState('networkidle', { timeout: NETWORK_IDLE_TIMEOUT_MS }).catch(() => {}); + + await pm.alertsPage.expectFrequencyCell(cronName, '0 */10 * * * *'); + await pm.alertsPage.expectFrequencyCell(minsName, '10 Mins'); + await pm.alertsPage.expectFrequencyCell(rtName, '--'); + testLogger.info('Frequency cells render cron, minutes and realtime cadence correctly'); + }); + + test('Check every column is present on scheduled and absent on the realTime tab', { + tag: ['@alert-list-frequency-retries', '@all', '@alerts', '@P0'], + }, async ({ page }) => { + const suffix = Math.random().toString(36).substring(2, 8); + const schedName = 'auto_tab_sched_' + suffix; + const rtName = 'auto_tab_rt_' + suffix; + createdAlertNames.push(schedName, rtName); + + // One scheduled + one real-time alert so both tabs render a table header to assert against. + await createAlert(page, simpleAlert(schedName)); + await createAlert(page, realtimeAlert(rtName)); + + await page.goto(`${logData.alertUrl}?org_identifier=${getOrgIdentifier()}`); + await page.waitForLoadState('networkidle', { timeout: NETWORK_IDLE_TIMEOUT_MS }).catch(() => {}); + + await pm.alertsPage.clickAlertTypeTab('scheduled'); + await pm.alertsPage.expectListColumns({ present: ['Check every'], absent: [] }); + + await pm.alertsPage.clickAlertTypeTab('realTime'); + await pm.alertsPage.expectListColumns({ present: [], absent: ['Check every'] }); + testLogger.info('Check every column present on scheduled, absent on realTime'); + }); + + // ───────────────────────────────────────────────────────────────────────── + // Retries column in the per-alert evaluation history + // ───────────────────────────────────────────────────────────────────────── + test('evaluation history shows a Retries column with a numeric value', { + tag: ['@alert-list-frequency-retries', '@all', '@alerts', '@P1'], + }, async ({ page }) => { + const suffix = Math.random().toString(36).substring(2, 8); + const retryName = 'auto_retry_' + suffix; + createdAlertNames.push(retryName); + + // Fast cadence so the scheduler evaluates it within the waitForAlertOutcome window. + const retryAlert = simpleAlert(retryName); + retryAlert.trigger_condition.frequency = 1; + const createResp = await createAlert(page, retryAlert); + expect(createResp.ok(), `alert create failed: ${createResp.status()}`).toBeTruthy(); + + // Guarantee a recent evaluation so the history endpoint returns a hit. + const evaluated = await waitForAlertOutcome(page, retryName); + expect(evaluated, `scheduled alert ${retryName} must be evaluated within the timeout`).toBeTruthy(); + expect(evaluated.last_outcome, 'the alert must have a run outcome').toBeTruthy(); + + const id = await findAlertId(page, retryName); + expect(id, 'the evaluated alert must be findable by name').toBeTruthy(); + + await pm.alertDetailPage.open(id); + await pm.alertsPage.expectAlertDetailsHistorySectionVisible(); + + await pm.alertsPage.expectEvaluationHistoryRetriesColumn(); + await pm.alertsPage.expectEvaluationHistoryRetriesValue(0); + testLogger.info('Retries column renders a numeric value in evaluation history'); + }); + + // ───────────────────────────────────────────────────────────────────────── + // Section tab strip agrees with the nav rail + // ───────────────────────────────────────────────────────────────────────── + test('the alert section tab strip shows the renamed labels', { + tag: ['@alert-list-frequency-retries', '@all', '@alerts', '@P1'], + }, async ({ page }) => { + await page.goto(`${logData.alertUrl}?org_identifier=${getOrgIdentifier()}`); + await page.waitForLoadState('networkidle', { timeout: NETWORK_IDLE_TIMEOUT_MS }).catch(() => {}); + + await pm.alertsPage.expectSectionTabLabels([ + 'All Alerts', 'Notification Destinations', 'Destination Templates', 'Alert Library', + ]); + await pm.alertsPage.expectSectionTabLabel('alertDestinations', 'Notification Destinations'); + await pm.alertsPage.expectSectionTabLabel('alertLibrary', 'Alert Library'); + testLogger.info('Section tab strip shows the renamed labels'); + }); }); diff --git a/tests/ui-testing/playwright-tests/utils/alerts-api-helpers.js b/tests/ui-testing/playwright-tests/utils/alerts-api-helpers.js index 3437bdd803..d8ff6f42b5 100644 --- a/tests/ui-testing/playwright-tests/utils/alerts-api-helpers.js +++ b/tests/ui-testing/playwright-tests/utils/alerts-api-helpers.js @@ -81,6 +81,18 @@ function realtimeAlert(name) { return a; } +/** A scheduled cron alert whose cadence cell renders the raw cron string verbatim. */ +function cronAlert(name) { + const a = simpleAlert(name); + a.trigger_condition = { + ...a.trigger_condition, + frequency_type: 'cron', + frequency: 1, + cron: '0 */10 * * * *', + }; + return a; +} + /** V1 composite payload. Operands are stable alert IDs, never display names. */ function compositeAlert(name, childIds, overrides = {}) { return { @@ -228,7 +240,7 @@ function isFiringOutcome(outcome) { module.exports = { BASE, STREAM, SINK, TMPL, DEST, uniq, urls, api, - simpleAlert, multiAlert, groupedSimpleAlert, realtimeAlert, + simpleAlert, multiAlert, groupedSimpleAlert, realtimeAlert, cronAlert, compositeAlert, validateComposite, getCompositeReferences, createAlert, listAlerts, findAlertId, getAlert, deleteAlerts, seedAlertFixtures, ingest, getAlertGroups, getAlertTransitions,