test(autoe2e): generated E2E tests for #15351 (create-alert-from-panel) (#15461)

Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
This commit is contained in:
Shrinath Rao
2026-10-22 13:20:24 +00:00
committed by GitHub
co-authored by Shrinath-O2 Abhay Padamani
parent e4811f206b
commit 879dbcaebf
6 changed files with 479 additions and 2 deletions
+3 -2
View File
@@ -29,7 +29,8 @@
"alertDestinationsPagination.spec.js",
"alerts-destinations-usage.spec.js",
"alerts-save-query-mode.spec.js",
"alerts-no-destination.spec.js"
"alerts-no-destination.spec.js",
"alerts-from-panel.spec.js"
],
"disabled": [
{
@@ -561,7 +562,7 @@
},
{
"testfolder": "Synthetics",
"_comment": "Own shard because the synthetics routes only exist when the server runs with ZO_SYNTHETICS_ENABLED=true (router registers /api/{org}/synthetics* conditionally). The flag is read once at process start, so it cannot vary between tests sharing a backend — each shard gets its own OpenObserve process; every other shard omits synthetics_enabled and keeps the default (false). Pinned to 3 workers: every create/list test writes a check and 5 concurrent writers trip SQLite's 'database is locked' (SLO shard precedent). Results/run-detail pages are fed by seeding the synthetics_results streams in beforeAll — no probe runs on CI. status-pages-enterprise-gating lives here because the status-page routes sit inside the same gate.",
"_comment": "Own shard because the synthetics routes only exist when the server runs with ZO_SYNTHETICS_ENABLED=true (router registers /api/{org}/synthetics* conditionally). The flag is read once at process start, so it cannot vary between tests sharing a backend \u2014 each shard gets its own OpenObserve process; every other shard omits synthetics_enabled and keeps the default (false). Pinned to 3 workers: every create/list test writes a check and 5 concurrent writers trip SQLite's 'database is locked' (SLO shard precedent). Results/run-detail pages are fed by seeding the synthetics_results streams in beforeAll \u2014 no probe runs on CI. status-pages-enterprise-gating lives here because the status-page routes sit inside the same gate.",
"actual_folder": "Synthetics",
"browser": "chrome",
"synthetics_enabled": "true",
@@ -3255,6 +3255,11 @@ export class AlertsPage {
return ((await valueEl.textContent()) ?? '').trim();
}
/** Display-mode alert name value element (OFormInlineEdit span, not an <input>). */
getAlertNameValueLocator() {
return this.page.locator('[data-test="add-alert-name-input-value"]').first();
}
/**
* Select stream type from dropdown
* OSelect popover pattern: open trigger, wait for popover, click option keyed by data-test-value (§4)
@@ -0,0 +1,94 @@
// Create Alert From Source confirm-dialog page object.
// The shared "create an alert from here" confirmation step (app-root mounted via
// CreateAlertDialogProvider in App.vue). Every section renders off the prefill,
// so this owns the dialog shell + the conditional sections the specs assert on.
import { expect } from "@playwright/test";
export default class CreateAlertFromSourceDialogPage {
constructor(page) {
this.page = page;
// Dialog shell + gating visibility (continue/cancel are ODialog buttons inside it).
this.dialog = page.locator('[data-test="create-alert-from-source-dialog"]');
this.continueBtn = page.locator(
'[data-test="create-alert-from-source-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.cancelBtn = page.locator(
'[data-test="create-alert-from-source-dialog"] [data-test="o-dialog-secondary-btn"]'
);
// Query-picker mode (pickQueryOnly): "pick which query" + choice OSelect + picked text.
this.queryPickerIntro = page.locator('[data-test="create-alert-query-picker-intro"]');
this.queryPicker = page.locator('[data-test="create-alert-query-picker"]');
this.queryPickerTrigger = page.locator('[data-test="create-alert-query-picker-trigger"]');
this.queryPickerOption = page.locator('[data-test="create-alert-query-picker-option"]');
this.pickedQuery = page.locator('[data-test="create-alert-picked-query"]');
this.pickedQueryText = page.locator('[data-test="create-alert-picked-query-text"]');
// Sections suppressed while pickQueryOnly is active (multi-query panel).
this.thresholdShape = page.locator('[data-test="create-alert-threshold-shape"]');
this.window = page.locator('[data-test="create-alert-window"]');
this.queryPreview = page.locator('[data-test="create-alert-query-preview"]');
}
async expectDialogVisible() {
await expect(this.dialog).toBeVisible({ timeout: 15000 });
}
async expectDialogHidden() {
await expect(this.dialog).toBeHidden({ timeout: 15000 });
}
async expectQueryPickerVisible() {
await expect(this.queryPicker).toBeVisible({ timeout: 10000 });
}
async expectPickedQueryTextVisible() {
await expect(this.pickedQueryText).toBeVisible({ timeout: 10000 });
}
/** The picked-query text (colorized SQL), trimmed; empty while unset. */
async getPickedQueryText() {
await this.pickedQueryText.waitFor({ state: "visible", timeout: 10000 });
return (await this.pickedQueryText.textContent())?.trim() ?? "";
}
async expectThresholdShapeHidden() {
await expect(this.thresholdShape).toHaveCount(0);
}
async expectWindowHidden() {
await expect(this.window).toHaveCount(0);
}
async expectQueryPreviewHidden() {
await expect(this.queryPreview).toHaveCount(0);
}
/** Open the query picker and choose the query at the given (0-based) index. */
async selectQuery(index) {
await this.queryPickerTrigger.click();
await this.queryPickerOption
.nth(index)
.waitFor({ state: "visible", timeout: 10000 });
await this.queryPickerOption.nth(index).click();
}
async clickContinue() {
await this.continueBtn.click();
}
async clickCancel() {
await this.cancelBtn.click();
}
async expectContinueDisabled() {
await expect(this.continueBtn).toBeDisabled();
}
async expectWarningVisible(key) {
await expect(
this.page.locator(`[data-test="create-alert-warning-${key}"]`)
).toBeVisible({ timeout: 10000 });
}
}
@@ -325,6 +325,18 @@ export default class DashboardPanel {
return this.page.locator('[data-test="dashboard-panel-container"]');
}
// Create Alert menu item (dropdown item) for disabled/state assertions.
getCreateAlertFromPanelItem() {
return this.createAlertFromPanel;
}
// The disabled-reason description rendered under the item (ODropdownItem).
getCreateAlertFromPanelReason() {
return this.page.locator(
'[data-test="dashboard-create-alert-from-panel-reason"]'
);
}
// Alert context menu options (shown on chart right-click).
getAlertContextMenuAbove() {
return this.alertContextMenuAbove;
+2
View File
@@ -31,6 +31,7 @@ import LogsVisualise from "./dashboardPages/visualise";
import { DashboardPage } from "./dashboardPages/dashboardPage.js";
import { ScheduledReportsDrawerPage } from "./dashboardPages/scheduledReportsDrawer.js";
import { AlertsPage } from "./alertsPages/alertsPage.js";
import CreateAlertFromSourceDialogPage from "./alertsPages/createAlertFromSourceDialogPage.js";
import { AlertLibraryPage } from "./alertsPages/alertLibraryPage.js";
import { AlertHistoryPage } from "./alertsPages/alertHistoryPage.js";
import { AlertDetailPage } from "./alertsPages/alertDetailPage.js";
@@ -201,6 +202,7 @@ class PageManager {
// ===== EXISTING ALERTS PAGE OBJECT =====
this.alertsPage = new AlertsPage(page);
this.createAlertFromSourceDialog = new CreateAlertFromSourceDialogPage(page);
this.alertLibraryPage = new AlertLibraryPage(page);
this.alertHistoryPage = new AlertHistoryPage(page);
this.alertDetailPage = new AlertDetailPage(page);
@@ -0,0 +1,363 @@
// Create Alert from Dashboard Panel - E2E Tests
// Covers the shared "create an alert from here" confirm dialog when entered from
// a dashboard panel (source: "panel"). The headline path is a multi-query panel
// opening the dialog in pickQueryOnly mode (query picker + picked query, with
// threshold/window/preview suppressed), plus the disabled menu item and cancel.
import {
test,
expect,
navigateToBase,
} from "../utils/enhanced-baseFixtures.js";
import PageManager from "../../pages/page-manager";
import { ingestion } from "../Dashboards/utils/dashIngestion.js";
import {
waitForDashboardPage,
deleteDashboard,
} from "../Dashboards/utils/dashCreation.js";
import { waitForStreamComplete } from "../utils/streaming-helpers.js";
import testLogger from "../utils/test-logger.js";
test.describe("Create Alert from Dashboard Panel testcases", () => {
test.describe.configure({ mode: "parallel" });
let pm;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
await navigateToBase(page);
pm = new PageManager(page);
await ingestion(page);
// navigateToBase() alone can land on the wrong org's page on cloud; force
// the correct org with an explicit navigation to a real feature page.
await page.goto(
`${process.env["ZO_BASE_URL"]}/web/dashboards?org_identifier=${process.env["ORGNAME"]}&folder=default`
);
await page.waitForLoadState("domcontentloaded");
testLogger.info("Test setup completed");
});
test(
"should open the query-picker confirm dialog from a multi-query panel and land on the pre-filled alert form",
{ tag: ["@create-alert-from-panel", "@alerts", "@all", "@P0"] },
async ({ page }) => {
testLogger.info("Testing multi-query panel -> confirm dialog -> alert form");
const dashName =
"Dashboard_Alert_MQ_" + Math.random().toString(36).slice(2, 11);
const panelName =
pm.dashboardPanelActions.generateUniquePanelName("alert-mq");
await pm.dashboardList.menuItem("dashboards-item");
await waitForDashboardPage(page);
await pm.dashboardCreate.createDashboard(dashName);
// Bar chart panel with two visible queries, each on e2e_automate.
await pm.dashboardCreate.addPanel();
await pm.dashboardPanelActions.addPanelName(panelName);
// Q1
await pm.chartTypeSelector.selectChartType("bar");
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_container_name",
"y"
);
// Q2 — a new query tab becomes active; give it its own stream + measure.
await pm.dashboardMultiSQL.addQueryTab(1);
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_namespace_name",
"y"
);
const streamPromise = waitForStreamComplete(page);
await pm.dashboardPanelActions.applyDashboardBtn();
await streamPromise;
await pm.dashboardPanelActions.waitForChartToRender();
const dashboardStreamPromise = waitForStreamComplete(page, 30000);
await pm.dashboardPanelActions.savePanel();
await dashboardStreamPromise;
await pm.dashboardPanelActions
.getChartRendererCanvasElement()
.first()
.waitFor({ state: "visible", timeout: 15000 });
await pm.dashboardPanelEdit.getPanelContainer().first().hover();
await pm.dashboardPanelEdit.createAlertFromPanelMenu(panelName);
// The confirm dialog appears only because there are >= 2 queries.
await pm.createAlertFromSourceDialog.expectDialogVisible();
await pm.createAlertFromSourceDialog.expectQueryPickerVisible();
// pickQueryOnly shows the picked query; threshold/window/preview are suppressed.
await pm.createAlertFromSourceDialog.expectPickedQueryTextVisible();
expect(
await pm.createAlertFromSourceDialog.getPickedQueryText()
).not.toBe("");
await pm.createAlertFromSourceDialog.expectThresholdShapeHidden();
await pm.createAlertFromSourceDialog.expectWindowHidden();
await pm.createAlertFromSourceDialog.expectQueryPreviewHidden();
await pm.createAlertFromSourceDialog.clickContinue();
await page.waitForURL(/.*alerts\/add.*prefill=panel.*/, {
timeout: 30000,
});
const currentUrl = page.url();
expect(currentUrl).toContain("prefill=panel");
// The panel payload rides sessionStorage, not the URL.
expect(currentUrl).not.toContain("panelData=");
// The alert name is an OFormInlineEdit title: in display mode the value
// is a span, not an <input>, so read the committed value via the helper.
const alertNameValue = pm.alertsPage.getAlertNameValueLocator();
await expect(alertNameValue).toBeVisible({ timeout: 30000 });
expect((await alertNameValue.textContent()) ?? "").toContain("Alert_from_");
testLogger.info("Landed on the alert form pre-filled from the panel");
// Navigate back to the dashboard folder and clean up.
await page.goto(
`${process.env["ZO_BASE_URL"]}/web/dashboards?org_identifier=${process.env["ORGNAME"]}&folder=default`
);
await page.waitForLoadState("domcontentloaded");
await waitForDashboardPage(page);
await pm.dashboardFolder
.getFolderCardByName("default")
.waitFor({ state: "visible", timeout: 30000 });
await pm.dashboardFolder.openFolderByName("default");
await deleteDashboard(page, dashName);
testLogger.info("Test completed: multi-query panel confirm dialog");
}
);
test(
"should update the picked-query text when a different query is selected in the dialog",
{ tag: ["@create-alert-from-panel", "@alerts", "@all", "@P1"] },
async ({ page }) => {
testLogger.info("Testing query picker re-parameterisation (rebuild)");
const dashName =
"Dashboard_Alert_RB_" + Math.random().toString(36).slice(2, 11);
const panelName =
pm.dashboardPanelActions.generateUniquePanelName("alert-rebuild");
await pm.dashboardList.menuItem("dashboards-item");
await waitForDashboardPage(page);
await pm.dashboardCreate.createDashboard(dashName);
await pm.dashboardCreate.addPanel();
await pm.dashboardPanelActions.addPanelName(panelName);
await pm.chartTypeSelector.selectChartType("bar");
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_container_name",
"y"
);
await pm.dashboardMultiSQL.addQueryTab(1);
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_namespace_name",
"y"
);
const streamPromise = waitForStreamComplete(page);
await pm.dashboardPanelActions.applyDashboardBtn();
await streamPromise;
await pm.dashboardPanelActions.waitForChartToRender();
const dashboardStreamPromise = waitForStreamComplete(page, 30000);
await pm.dashboardPanelActions.savePanel();
await dashboardStreamPromise;
await pm.dashboardPanelActions
.getChartRendererCanvasElement()
.first()
.waitFor({ state: "visible", timeout: 15000 });
await pm.dashboardPanelEdit.getPanelContainer().first().hover();
await pm.dashboardPanelEdit.createAlertFromPanelMenu(panelName);
await pm.createAlertFromSourceDialog.expectDialogVisible();
const initialText =
await pm.createAlertFromSourceDialog.getPickedQueryText();
expect(initialText).not.toBe("");
await pm.createAlertFromSourceDialog.selectQuery(1);
// The picked-query display follows the newly chosen query.
await expect
.poll(
async () =>
await pm.createAlertFromSourceDialog.getPickedQueryText(),
{ timeout: 15000 }
)
.not.toBe(initialText);
await pm.createAlertFromSourceDialog.clickCancel();
await pm.createAlertFromSourceDialog.expectDialogHidden();
await pm.dashboardCreate.backToDashboardList();
await deleteDashboard(page, dashName);
testLogger.info("Test completed: query picker rebuild");
}
);
test(
"should close the confirm dialog on Cancel and stay on the dashboard",
{ tag: ["@create-alert-from-panel", "@alerts", "@all", "@P1"] },
async ({ page }) => {
testLogger.info("Testing Cancel on the confirm dialog");
const dashName =
"Dashboard_Alert_Cancel_" + Math.random().toString(36).slice(2, 11);
const panelName =
pm.dashboardPanelActions.generateUniquePanelName("alert-cancel");
await pm.dashboardList.menuItem("dashboards-item");
await waitForDashboardPage(page);
await pm.dashboardCreate.createDashboard(dashName);
await pm.dashboardCreate.addPanel();
await pm.dashboardPanelActions.addPanelName(panelName);
await pm.chartTypeSelector.selectChartType("bar");
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_container_name",
"y"
);
await pm.dashboardMultiSQL.addQueryTab(1);
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_namespace_name",
"y"
);
const streamPromise = waitForStreamComplete(page);
await pm.dashboardPanelActions.applyDashboardBtn();
await streamPromise;
await pm.dashboardPanelActions.waitForChartToRender();
const dashboardStreamPromise = waitForStreamComplete(page, 30000);
await pm.dashboardPanelActions.savePanel();
await dashboardStreamPromise;
await pm.dashboardPanelActions
.getChartRendererCanvasElement()
.first()
.waitFor({ state: "visible", timeout: 15000 });
await pm.dashboardPanelEdit.getPanelContainer().first().hover();
await pm.dashboardPanelEdit.createAlertFromPanelMenu(panelName);
await pm.createAlertFromSourceDialog.expectDialogVisible();
await pm.createAlertFromSourceDialog.clickCancel();
await pm.createAlertFromSourceDialog.expectDialogHidden();
// No route change: still on the dashboard, not the alert form.
expect(page.url()).not.toContain("alerts/add");
await pm.dashboardCreate.backToDashboardList();
await deleteDashboard(page, dashName);
testLogger.info("Test completed: Cancel closes the confirm dialog");
}
);
test(
"should disable the Create Alert menu item with a reason when the panel has no visible query",
{ tag: ["@create-alert-from-panel", "@alerts", "@all", "@P1"] },
async ({ page }) => {
testLogger.info("Testing disabled Create Alert menu item (no visible query)");
const dashName =
"Dashboard_Alert_Dsb_" + Math.random().toString(36).slice(2, 11);
const panelName =
pm.dashboardPanelActions.generateUniquePanelName("alert-disabled");
await pm.dashboardList.menuItem("dashboards-item");
await waitForDashboardPage(page);
await pm.dashboardCreate.createDashboard(dashName);
await pm.dashboardCreate.addPanel();
await pm.dashboardPanelActions.addPanelName(panelName);
await pm.chartTypeSelector.selectChartType("bar");
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_container_name",
"y"
);
// The visibility toggle only appears from the second query onwards, so
// add a second query and hide BOTH to leave the panel with no visible query.
await pm.dashboardMultiSQL.addQueryTab(1);
await pm.chartTypeSelector.selectStreamType("logs");
await pm.chartTypeSelector.selectStream("e2e_automate");
await pm.chartTypeSelector.searchAndAddField(
"kubernetes_namespace_name",
"y"
);
const streamPromise = waitForStreamComplete(page);
await pm.dashboardPanelActions.applyDashboardBtn();
await streamPromise;
await pm.dashboardPanelActions.waitForChartToRender();
// Hide both queries so the saved panel has zero visible queries.
await pm.dashboardMultiSQL.toggleQueryVisibility(0);
await pm.dashboardMultiSQL.toggleQueryVisibility(1);
const dashboardStreamPromise = waitForStreamComplete(page, 30000);
await pm.dashboardPanelActions.savePanel();
await dashboardStreamPromise;
await pm.dashboardPanelEdit.getPanelContainer().first().hover();
await pm.dashboardPanelActions.getEditPanelDropdown(panelName).click();
const createAlertItem = pm.dashboardPanelEdit.getCreateAlertFromPanelItem();
await expect(createAlertItem).toBeVisible({ timeout: 10000 });
await expect(createAlertItem).toBeDisabled();
await expect(
pm.dashboardPanelEdit.getCreateAlertFromPanelReason()
).toHaveText("This panel has no queries to create an alert from");
await pm.dashboardCreate.backToDashboardList();
await deleteDashboard(page, dashName);
testLogger.info("Test completed: disabled Create Alert menu item");
}
);
test.fixme(
"should block Continue in the confirm dialog when the prefill carries an unresolved-query warning — deferred: unresolved-variable panel setup unproven in CI (fromPanel.ts:293-295)",
{ tag: ["@create-alert-from-panel", "@alerts", "@all", "@P2"] },
async ({ page }) => {
// A panel query referencing an unresolved dashboard variable ($var) with
// no executedQuery produces a blocking `unresolvedQuery` warning, which
// disables Continue. Constructing that panel deterministically (a $var
// that does not resolve) has no proven helper in the suite yet.
testLogger.info("Testing blocking warning disables Continue (deferred)");
await pm.createAlertFromSourceDialog.expectWarningVisible("unresolvedQuery");
await pm.createAlertFromSourceDialog.expectContinueDisabled();
}
);
});