diff --git a/tests/ui-testing/ci-matrix/ci_matrix.json b/tests/ui-testing/ci-matrix/ci_matrix.json index 4c4b23b80d..34bfe3b4ee 100644 --- a/tests/ui-testing/ci-matrix/ci_matrix.json +++ b/tests/ui-testing/ci-matrix/ci_matrix.json @@ -31,7 +31,8 @@ "alerts-destinations-usage.spec.js", "alerts-save-query-mode.spec.js", "alerts-no-destination.spec.js", - "alerts-from-panel.spec.js" + "alerts-from-panel.spec.js", + "alert-calibration.spec.js" ], "disabled": [ { diff --git a/tests/ui-testing/pages/alertsPages/alertCalibrationPage.js b/tests/ui-testing/pages/alertsPages/alertCalibrationPage.js new file mode 100644 index 0000000000..e6b8497934 --- /dev/null +++ b/tests/ui-testing/pages/alertsPages/alertCalibrationPage.js @@ -0,0 +1,119 @@ +// Copyright 2026 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// alertCalibrationPage.js - Page Object for the Alert Calibration views +// (web/src/components/alert_calibration/*). The feature is gated behind an +// enterprise/cloud build plus `alert_calibration_enabled` from the live +// `/api/{org}/config` response; the OSS route guard bounces `/alert-calibration` +// to the alert list. Selectors are data-test only. +import { expect } from '@playwright/test'; + +export class AlertCalibrationPage { + constructor(page) { + this.page = page; + + // Digests list + this.digestsPage = '[data-test="alert-calibration-digests-page"]'; + this.digestsTable = '[data-test="alert-calibration-digests-table"]'; + this.digestsStats = '[data-test="alert-calibration-digests-stats"]'; + + // Dashboard + this.dashboardPage = '[data-test="alert-calibration-dashboard-page"]'; + + // Settings + this.settingsPage = '[data-test="alert-calibration-settings"]'; + this.settingsForm = '[data-test="alert-calibration-settings-form"]'; + + // Tabs and actions + this.tabs = '[data-test="alert-calibration-tabs"]'; + this.tabDashboard = '[data-test="alert-calibration-tab-alertCalibrationDashboard"]'; + this.tabDigests = '[data-test="alert-calibration-tab-alertCalibration"]'; + + // Redirect target for the OSS route guard + this.alertListPage = '[data-test="alert-list-page"]'; + } + + /** + * Reads the feature gate from the live `/api/{org}/config` response: the + * `build_type` and `alert_calibration_enabled` fields are exactly what the + * route guard and nav gate read off the Vuex store, which is hydrated from + * this same endpoint. Reads the backend directly (no dev proxy for /api), + * mirroring StatusPagesPage.detectBuildType. + */ + async detectGate(org) { + const orgId = org || process.env['ORGNAME'] || 'default'; + const baseUrl = (process.env['INGESTION_URL'] || process.env['ZO_BASE_URL']).replace(/\/+$/, ''); + const response = await this.page.request.get(`${baseUrl}/api/${orgId}/config`); + expect(response.ok(), 'reading /api/{org}/config should succeed').toBe(true); + const body = await response.json(); + return { + buildType: body.build_type, + enabled: body.alert_calibration_enabled === true, + }; + } + + async gotoDigests(org) { + const orgId = org || process.env['ORGNAME'] || 'default'; + await this.page.goto( + `${process.env['ZO_BASE_URL']}/web/alert-calibration?org_identifier=${orgId}`, + { waitUntil: 'domcontentloaded' }, + ); + await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); + } + + async gotoDashboard(org) { + const orgId = org || process.env['ORGNAME'] || 'default'; + await this.page.goto( + `${process.env['ZO_BASE_URL']}/web/alert-calibration/dashboard?org_identifier=${orgId}`, + { waitUntil: 'domcontentloaded' }, + ); + await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); + } + + async gotoSettings(org) { + const orgId = org || process.env['ORGNAME'] || 'default'; + await this.page.goto( + `${process.env['ZO_BASE_URL']}/web/alert-calibration/settings?org_identifier=${orgId}`, + { waitUntil: 'domcontentloaded' }, + ); + await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); + } + + async expectDigestsPageVisible() { + await expect(this.page.locator(this.digestsPage)).toBeVisible({ timeout: 15000 }); + } + + async expectDashboardPageVisible() { + await expect(this.page.locator(this.dashboardPage)).toBeVisible({ timeout: 15000 }); + } + + async expectSettingsVisible() { + await expect(this.page.locator(this.settingsPage)).toBeVisible({ timeout: 15000 }); + } + + async expectSettingsFormVisible() { + await expect(this.page.locator(this.settingsForm)).toBeVisible({ timeout: 15000 }); + } + + /** The OSS guard replaces the route with alertList, so the alert list renders. */ + async expectRedirectedToAlertList() { + await expect(this.page.locator(this.alertListPage)).toBeVisible({ timeout: 15000 }); + } + + async clickDashboardTab() { + await this.page.locator(this.tabDashboard).click(); + await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {}); + } +} diff --git a/tests/ui-testing/pages/page-manager.js b/tests/ui-testing/pages/page-manager.js index 9b2dfdfc68..bd6b240aa6 100644 --- a/tests/ui-testing/pages/page-manager.js +++ b/tests/ui-testing/pages/page-manager.js @@ -42,6 +42,7 @@ import { AlertSaveQueryModePage } from "./alertsPages/alertSaveQueryModePage.js" import { AlertNoDestinationPage } from "./alertsPages/alertNoDestinationPage.js"; import { CompositeAlertsPage } from "./alertsPages/compositeAlertsPage.js"; import { IncidentsPage } from "./alertsPages/incidentsPage.js"; +import { AlertCalibrationPage } from "./alertsPages/alertCalibrationPage.js"; import { SloListPage } from "./sloPages/sloListPage.js"; import { SloFormPage } from "./sloPages/sloFormPage.js"; import { SloDetailPage } from "./sloPages/sloDetailPage.js"; @@ -224,6 +225,7 @@ class PageManager { this.alertNoDestinationPage = new AlertNoDestinationPage(page); this.compositeAlertsPage = new CompositeAlertsPage(page); this.incidentsPage = new IncidentsPage(page); + this.alertCalibrationPage = new AlertCalibrationPage(page); // ===== SLO PAGE OBJECTS ===== this.sloListPage = new SloListPage(page); diff --git a/tests/ui-testing/playwright-tests/Alerts/alert-calibration.spec.js b/tests/ui-testing/playwright-tests/Alerts/alert-calibration.spec.js new file mode 100644 index 0000000000..28754132dd --- /dev/null +++ b/tests/ui-testing/playwright-tests/Alerts/alert-calibration.spec.js @@ -0,0 +1,78 @@ +// Copyright 2026 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Enterprise-gated: OSS only exercises the route-guard redirect; the feature views render only when alert_calibration_enabled is on. + +const { test, navigateToBase } = require('../utils/enhanced-baseFixtures.js'); +const testLogger = require('../utils/test-logger.js'); +const PageManager = require('../../pages/page-manager.js'); + +test.describe('Alert Calibration testcases', { + tag: ['@alert-calibration', '@all'], +}, () => { + test.describe.configure({ mode: 'parallel' }); + + let pm; + let gate = { buildType: null, enabled: false }; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext({ storageState: 'playwright-tests/utils/auth/user.json' }); + const page = await context.newPage(); + const probe = new PageManager(page); + gate = await probe.alertCalibrationPage.detectGate(); + await context.close(); + }); + + test.beforeEach(async ({ page }, testInfo) => { + testLogger.testStart(testInfo.title, testInfo.file); + await navigateToBase(page); + pm = new PageManager(page); + }); + + test('the alert calibration route redirects to the alert list on an OSS build', { + tag: ['@oss', '@P0'], + }, async () => { + test.skip(gate.buildType !== 'opensource', `Runs only on OSS build (detected: ${gate.buildType})`); + await pm.alertCalibrationPage.gotoDigests(); + await pm.alertCalibrationPage.expectRedirectedToAlertList(); + }); + + test('the digests list renders its page shell with the feature enabled', { + tag: ['@enterprise', '@P0'], + }, async () => { + test.skip(!gate.enabled, 'Alert Calibration is disabled (requires enterprise + alert_calibration_enabled)'); + await pm.alertCalibrationPage.gotoDigests(); + await pm.alertCalibrationPage.expectDigestsPageVisible(); + }); + + test('the dashboard opens from the digests tab', { + tag: ['@enterprise', '@P1'], + }, async () => { + test.skip(!gate.enabled, 'Alert Calibration is disabled (requires enterprise + alert_calibration_enabled)'); + await pm.alertCalibrationPage.gotoDigests(); + await pm.alertCalibrationPage.expectDigestsPageVisible(); + await pm.alertCalibrationPage.clickDashboardTab(); + await pm.alertCalibrationPage.expectDashboardPageVisible(); + }); + + test('the settings page renders the configuration form', { + tag: ['@enterprise', '@P1'], + }, async () => { + test.skip(!gate.enabled, 'Alert Calibration is disabled (requires enterprise + alert_calibration_enabled)'); + await pm.alertCalibrationPage.gotoSettings(); + await pm.alertCalibrationPage.expectSettingsVisible(); + await pm.alertCalibrationPage.expectSettingsFormVisible(); + }); +});