test(autoe2e): generated E2E tests for #14601 (dashboard-cell-explorer-log-detail) (#14703)

Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com>
Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
Co-authored-by: Neha P <neha@openobserve.ai>
This commit is contained in:
Shrinath Rao
2026-09-22 11:32:05 +00:00
committed by GitHub
co-authored by Shrinath-O2 ktx-akshay Neha P
parent 930b534d7b
commit 0b1e67bc2c
4 changed files with 539 additions and 20 deletions
+58 -19
View File
@@ -34,14 +34,14 @@ from __future__ import annotations
import logging
import time
import uuid
from collections.abc import Generator
from collections.abc import Callable, Generator
from typing import Any
import pytest
from support.client import OpenObserveClient
from support.factories import unique_name
from support.wait import wait_until
from support.wait import WaitTimeout, wait_until
logger = logging.getLogger(__name__)
@@ -145,6 +145,37 @@ def _alert_payload(
}
def _get_alert_until(
client: OpenObserveClient,
alert_id: str,
predicate: Callable[[dict[str, Any]], bool],
*,
msg: str,
timeout: float = 30.0,
) -> dict[str, Any]:
"""GET the alert, polling until `predicate` holds over the response body.
A write does not update the server's in-memory alert cache directly: it
writes the row and emits a coordinator watch event, and `get_by_id` serves
the cached copy until the watcher applies that event. So a GET issued right
after a PUT/PATCH can still return the pre-update alert. Every assertion
about a value a test just wrote has to poll for it.
"""
seen: dict[str, Any] = {}
def _matches() -> bool:
resp = client.get(f"alerts/{alert_id}", prefix="api/v2/")
assert resp.status_code == 200, f"{resp.status_code}: {resp.text}"
seen["body"] = resp.json()
return predicate(seen["body"])
try:
wait_until(_matches, timeout=timeout, interval=0.5, msg=msg)
except WaitTimeout as e:
raise AssertionError(f"{e}; last alert body: {seen.get('body')!r}") from e
return seen["body"]
# ----- fixtures -----
@@ -310,10 +341,12 @@ def test_update_alert_description_field(
)
assert resp.status_code == 200, f"update failed: {resp.status_code} {resp.text}"
# Verify the change persisted
resp_get = client.get(f"alerts/{temp_alert['alert_id']}", prefix="api/v2/")
assert resp_get.json().get("description") == new_description, \
"description should be persisted after update"
_get_alert_until(
client,
temp_alert["alert_id"],
lambda body: body.get("description") == new_description,
msg=f"description should be persisted after update: {new_description!r}",
)
# ----- enable / disable -----
@@ -332,13 +365,15 @@ def test_disable_alert_via_enable_endpoint(
)
assert resp.status_code == 200, f"disable failed: {resp.status_code} {resp.text}"
resp_get = client.get(f"alerts/{alert_id}", prefix="api/v2/")
assert resp_get.status_code == 200
enabled = resp_get.json().get("enabled")
# OO returns boolean False (not the string 'False' — the original test
# had that bug AND the assertion was unreachable due to an indentation
# error inside an else block that never ran).
assert enabled is False, f"alert should be disabled (enabled=False), got: {enabled!r}"
_get_alert_until(
client,
alert_id,
lambda body: body.get("enabled") is False,
msg="alert should be disabled (enabled=False)",
)
def test_enable_alert_via_enable_endpoint(
@@ -358,9 +393,12 @@ def test_enable_alert_via_enable_endpoint(
)
assert resp.status_code == 200, f"enable failed: {resp.status_code} {resp.text}"
resp_get = client.get(f"alerts/{alert_id}", prefix="api/v2/")
enabled = resp_get.json().get("enabled")
assert enabled is True, f"alert should be enabled (enabled=True), got: {enabled!r}"
_get_alert_until(
client,
alert_id,
lambda body: body.get("enabled") is True,
msg="alert should be enabled (enabled=True)",
)
# ----- trigger -----
@@ -587,12 +625,13 @@ def test_alert_vrl_function_can_be_updated(
assert resp_update.status_code == 200, f"update failed: {resp_update.status_code} {resp_update.text}"
# Verify both VRL change AND description change persisted
resp_get = client.get(f"alerts/{alert_id}", prefix="api/v2/")
body = resp_get.json()
assert body.get("query_condition", {}).get("vrl_function") == vrl_updated, \
"VRL should be updated to the new base64 string"
assert body.get("description") == "VRL update test - UPDATED", \
"description should be updated"
_get_alert_until(
client,
alert_id,
lambda body: body.get("query_condition", {}).get("vrl_function") == vrl_updated
and body.get("description") == "VRL update test - UPDATED",
msg="VRL and description should both be updated after PUT",
)
finally:
for a in client.alerts.list():
if a.get("name") == alert_name:
+2 -1
View File
@@ -165,7 +165,8 @@
"dashboard-table-column-filtering.spec.js",
"dashboard-table-pagination.spec.js",
"dashboard-pivot-table.spec.js",
"interactive-dashboard-table.spec.js"
"interactive-dashboard-table.spec.js",
"dashboard-cell-explorer-detail.spec.js"
],
"disabled": []
},
@@ -37,6 +37,31 @@ export default class DashboardCellExplorerPage {
// DateTime renders its `data-test-name` prop as the element's data-test.
this.dateTime = page.locator('[data-test="dashboard-log-drawer-date-time"]');
this.eventDetailDrawer = page.locator('[data-test="log-explorer-event-detail-drawer"]');
// In-place log-detail surface (replaces the results list when detailOpen)
this.rowOpen = page.locator('[data-test="log-explorer-row-open"]');
this.detailBackBtn = page.locator('[data-test="log-explorer-detail-back"]');
this.detailPrevBtn = page.locator('[data-test="log-explorer-detail-prev"]');
this.detailNextBtn = page.locator('[data-test="log-explorer-detail-next"]');
// Push-nav header counter ("n / m"); the span sits between the prev and
// next buttons and only renders while detail is open.
this.positionCounter = page
.locator('[data-test="log-explorer-detail-prev"] ~ span')
.first();
this.insightsTab = page.locator('[data-test="log-detail-insights-tab"]');
this.jsonTab = page.locator('[data-test="log-detail-json-tab"]');
this.tableTab = page.locator('[data-test="log-detail-table-tab"]');
this.jsonContent = page.locator('[data-test="log-detail-json-content"]');
this.tableContent = page.locator('[data-test="log-detail-table-content"]');
this.kvTable = page.locator('[data-test="log-detail-table"]');
this.kvSearchInput = page.locator('[data-test="log-detail-table-search-input-field"]');
// v-show gated on the table tab; present in the DOM on every tab.
this.wrapToggle = page.locator('[data-test="log-detail-wrap-values-toggle-btn"]');
this.surroundWindowBtn = page.locator('[data-test="log-explorer-surround-window"]');
// Copy-link OButton has no data-test; located by its i18n label.
this.copyLinkBtn = page.getByRole("button", { name: "Copy link" });
this.anomalyProfileHeader = page.getByText("Field Anomaly Profile").first();
this.surroundingHeader = page.getByText("Surrounding Events").first();
}
/**
@@ -136,4 +161,227 @@ export default class DashboardCellExplorerPage {
)
.toBe(false);
}
// ── In-place log-detail surface (DashboardLogDrawer detailOpen mode) ──────
/** Click the first result row's chevron and wait for the detail view. */
async openDetailFromFirstRow() {
await expect(this.resultsTable).toBeVisible({ timeout: 30000 });
await this.rowOpen.first().waitFor({ state: "visible", timeout: 15000 });
await this.rowOpen.first().click();
await expect(this.detailBackBtn).toBeVisible({ timeout: 30000 });
}
/** Detail is open: back button visible, results list hidden. */
async expectDetailOpen() {
await expect(this.detailBackBtn).toBeVisible({ timeout: 15000 });
await expect(this.resultsTable).toBeHidden({ timeout: 15000 });
}
/** Detail closed back to the list, but the cell-explorer drawer stays open. */
async expectDetailClosedDrawerOpen() {
await expect(this.resultsTable).toBeVisible({ timeout: 15000 });
await expect(this.detailBackBtn).toBeHidden({ timeout: 15000 });
await expect(this.cellDrawer).toBeVisible({ timeout: 15000 });
}
async clickBack() {
await this.detailBackBtn.click();
await expect(this.resultsTable).toBeVisible({ timeout: 30000 });
}
async pressEscape() {
await this.page.keyboard.press("Escape");
await expect(this.resultsTable).toBeVisible({ timeout: 15000 });
}
/** Field-anomaly profile renders synchronously on detail open. */
async expectAnomalyProfileVisible() {
await expect(this.anomalyProfileHeader).toBeVisible({ timeout: 15000 });
}
async expectDetailTabsVisible() {
await expect(this.insightsTab).toBeVisible({ timeout: 15000 });
await expect(this.jsonTab).toBeVisible({ timeout: 15000 });
await expect(this.tableTab).toBeVisible({ timeout: 15000 });
}
async clickJsonTab() {
await this.jsonTab.click();
await expect(this.jsonContent).toBeVisible({ timeout: 15000 });
}
async clickTableTab() {
await this.tableTab.click();
await expect(this.tableContent).toBeVisible({ timeout: 15000 });
await expect(this.kvTable).toBeVisible({ timeout: 15000 });
}
async expectWrapToggleVisible() {
await expect(this.wrapToggle).toBeVisible({ timeout: 15000 });
}
async expectWrapToggleHidden() {
await expect(this.wrapToggle).toBeHidden({ timeout: 15000 });
}
// ── Push-nav (prev/next) ─────────────────────────────────────────────────
async getPositionCurrent() {
const text = (await this.positionCounter.textContent()).trim();
return parseInt(text.split("/")[0].trim(), 10);
}
async getPositionTotal() {
const text = (await this.positionCounter.textContent()).trim();
return parseInt(text.split("/")[1].trim(), 10);
}
async expectNextEnabled() {
await expect(this.detailNextBtn).toBeEnabled({ timeout: 10000 });
}
async expectPrevEnabled() {
await expect(this.detailPrevBtn).toBeEnabled({ timeout: 10000 });
}
async expectPrevDisabled() {
await expect(this.detailPrevBtn).toBeDisabled({ timeout: 10000 });
}
async clickNext() {
await this.detailNextBtn.click();
}
async clickPrev() {
await this.detailPrevBtn.click();
}
// ── Shareable URL / copy-link ────────────────────────────────────────────
async expectCellEventTsInUrl() {
await expect
.poll(() => new URL(this.page.url()).searchParams.has("cell_event_ts"), {
timeout: 10000,
})
.toBe(true);
}
async expectCopyLinkVisible() {
await expect(this.copyLinkBtn).toBeVisible({ timeout: 15000 });
}
async clickCopyLink() {
await this.copyLinkBtn.click();
}
async getClipboardText() {
return this.page.evaluate(() => navigator.clipboard.readText());
}
// ── Surrounding events ───────────────────────────────────────────────────
async expectSurroundingSectionVisible() {
await expect(this.surroundingHeader).toBeVisible({ timeout: 30000 });
await expect(this.surroundWindowBtn).toBeVisible({ timeout: 15000 });
}
/**
* Change the surrounding-events window to `value` minutes. Returns the observed
* search window span (end_time - start_time, microseconds) of the re-fetch the
* change triggered, so the caller can assert the re-fetch actually used the NEW
* window rather than the initial ±3 min load.
*/
async changeSurroundWindow(value = "5") {
const minutes = Number(value);
const expectedSpan = 2 * minutes * 60 * 1_000_000;
const label = {
1: "±1 min",
2: "±2 min",
3: "±3 min",
5: "±5 min",
10: "±10 min",
}[minutes];
// Wait for the re-fetch: a surrounding-events search (`ORDER BY _timestamp
// ASC`) whose time window spans the NEW value, not the initial ±3 min.
const refetch = this.page.waitForResponse(
(resp) => {
const req = resp.request();
if (!req.url().includes("/_search")) return false;
let body;
try {
body = req.postDataJSON();
} catch {
return false;
}
if (
typeof body?.query?.sql !== "string" ||
!body.query.sql.includes("ORDER BY _timestamp ASC")
) {
return false;
}
const span =
Number(body.query.end_time) - Number(body.query.start_time);
return span === expectedSpan;
},
{ timeout: 30000 }
);
await this.surroundWindowBtn.click();
const option = this.page.locator(
`[data-test="log-explorer-surround-window-${value}"]`
);
// Reka dropdowns re-mount their items during the reveal animation, so a
// naive click flakes with "element was detached". Poll the click until it
// lands (option selected -> menu closes).
await expect(async () => {
await option.click({ timeout: 3000 });
}).toPass({ timeout: 15000, intervals: [500] });
// State guard: confirm the selection actually landed before trusting the
// downstream result.
await expect(this.surroundWindowBtn).toContainText(label);
const response = await refetch;
const body = response.request().postDataJSON();
return Number(body.query.end_time) - Number(body.query.start_time);
}
/** Surrounding section settled into rows or the (valid) empty state. */
async expectSurroundingResolved() {
const rows = this.page.locator(".dld-ctx-row");
const empty = this.page.getByText("No surrounding events found");
await expect
.poll(
async () =>
(await rows.count()) > 0 || (await empty.isVisible().catch(() => false)),
{ timeout: 30000 }
)
.toBe(true);
}
// ── Deep-link restore ────────────────────────────────────────────────────
async reloadAndWaitForDetail() {
await this.page.reload();
await expect(this.detailBackBtn).toBeVisible({ timeout: 60000 });
}
// ── KV table search ──────────────────────────────────────────────────────
async fillKvSearch(text) {
await this.kvSearchInput.fill(text);
}
async expectKvKeyVisible(field) {
await expect(
this.page.locator(`[data-test="log-detail-${field}-key"]`)
).toBeVisible({ timeout: 15000 });
}
async expectKvKeyHidden(field) {
await expect(
this.page.locator(`[data-test="log-detail-${field}-key"]`)
).toBeHidden({ timeout: 15000 });
}
}
@@ -0,0 +1,231 @@
// E2E — Dashboard Cell Explorer Log Detail (in-place log-detail surface)
// Feature slug: dashboard-cell-explorer-log-detail
//
// Covers the detail view INSIDE the cell-explorer drawer: open detail from a
// result row, back/Escape return, JSON/Table tabs, prev/next push-nav,
// copy-link URL share, surrounding-events window, deep-link restore, and KV
// search. Reuses the same e2e_automate fixture + drillable table panel as
// interactive-dashboard-table.spec.js (drawer-open path), which this spec does
// NOT duplicate.
const {
test,
expect,
navigateToBase,
} = require("../utils/enhanced-baseFixtures.js");
import PageManager from "../../pages/page-manager";
import { ingestion } from "./utils/dashIngestion.js";
import { cleanupTestDashboard } from "./utils/dashCreation.js";
import {
generateDashboardName,
setupTablePanelWithDimension,
} from "./utils/configPanelHelpers.js";
const testLogger = require("../utils/test-logger.js");
test.describe.configure({ mode: "parallel" });
test.describe.configure({ retries: 1 });
/**
* Build a drillable table panel, save it, and land on the dashboard VIEW page
* with the table rendered.
*/
async function buildDrillableTableAndView(page, pm, dashboardName) {
await setupTablePanelWithDimension(page, pm, dashboardName);
await pm.dashboardPanelActions.waitForChartToRender();
await pm.dashboardPanelActions.savePanel();
await pm.dashboardCellExplorer.waitForTableOnViewPage();
}
/**
* Drill into the first cell, then open the detail view for the first result.
*/
async function openDetail(pm) {
await pm.dashboardCellExplorer.openDrawerFromFirstDrillableCell();
await pm.dashboardCellExplorer.openDetailFromFirstRow();
}
test.describe("Dashboard Cell Explorer Log Detail", { tag: ["@all", "@dashboard", "@dashboard-cell-explorer-log-detail"] }, () => {
test.beforeEach(async ({ page }) => {
await navigateToBase(page);
await ingestion(page);
});
test("open detail from a drilled result row renders the detail surface", {
tag: ["@smoke", "@P0"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.expectDetailOpen();
await pm.dashboardCellExplorer.expectAnomalyProfileVisible();
await pm.dashboardCellExplorer.expectDetailTabsVisible();
testLogger.info("Detail surface rendered with back header, insights, and tabs");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("back returns to the results list and keeps the drawer open", {
tag: ["@smoke", "@P0"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.clickBack();
await pm.dashboardCellExplorer.expectDetailClosedDrawerOpen();
testLogger.info("Back returned to the results list without closing the drawer");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("JSON and Table tabs switch and render their content", {
tag: ["@functional", "@P1"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.clickJsonTab();
await pm.dashboardCellExplorer.expectWrapToggleHidden();
await pm.dashboardCellExplorer.clickTableTab();
await pm.dashboardCellExplorer.expectWrapToggleVisible();
testLogger.info("JSON and Table tabs render content; wrap toggle gated to table tab");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("prev/next steps between events and respects the position bounds", {
tag: ["@functional", "@P1"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
const total = await pm.dashboardCellExplorer.getPositionTotal();
expect(total).toBeGreaterThanOrEqual(2);
await pm.dashboardCellExplorer.expectNextEnabled();
await pm.dashboardCellExplorer.clickNext();
await pm.dashboardCellExplorer.expectPrevEnabled();
expect(await pm.dashboardCellExplorer.getPositionCurrent()).toBe(2);
await pm.dashboardCellExplorer.clickPrev();
await pm.dashboardCellExplorer.expectPrevDisabled();
expect(await pm.dashboardCellExplorer.getPositionCurrent()).toBe(1);
testLogger.info("Prev/next steps between events with correct bound handling");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("copy link exposes a shareable URL with cell_event_ts", {
tag: ["@functional", "@P1"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.expectCellEventTsInUrl();
await pm.dashboardCellExplorer.expectCopyLinkVisible();
await pm.dashboardCellExplorer.clickCopyLink();
await expect
.poll(() => pm.dashboardCellExplorer.getClipboardText(), { timeout: 10000 })
.toBe(page.url());
testLogger.info("Copy link shares a URL carrying cell_event_ts");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("surrounding events section and window dropdown render and re-fetch", {
tag: ["@functional", "@P1"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.expectSurroundingSectionVisible();
await pm.dashboardCellExplorer.expectSurroundingResolved();
const reFetchSpan = await pm.dashboardCellExplorer.changeSurroundWindow("5");
expect(reFetchSpan).toBe(2 * 5 * 60 * 1_000_000);
await pm.dashboardCellExplorer.expectSurroundingResolved();
testLogger.info("Surrounding events section rendered and re-fetched on window change");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("deep-link reload restores the detail view via cell_event_ts", {
tag: ["@functional", "@P2"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.expectCellEventTsInUrl();
await pm.dashboardCellExplorer.reloadAndWaitForDetail();
await pm.dashboardCellExplorer.expectDetailOpen();
testLogger.info("Detail view restored after reload from cell_event_ts URL param");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("Escape returns to the results list without closing the drawer", {
tag: ["@functional", "@P2"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.pressEscape();
await pm.dashboardCellExplorer.expectDetailClosedDrawerOpen();
testLogger.info("Escape returned to the list while the drawer stayed open");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
test("Table tab KV search filters the field list", {
tag: ["@functional", "@P2"],
}, async ({ page }) => {
const pm = new PageManager(page);
const dashboardName = generateDashboardName();
await buildDrillableTableAndView(page, pm, dashboardName);
await openDetail(pm);
await pm.dashboardCellExplorer.clickTableTab();
await pm.dashboardCellExplorer.expectKvKeyVisible("_timestamp");
await pm.dashboardCellExplorer.fillKvSearch("kubernetes_namespace_name");
await pm.dashboardCellExplorer.expectKvKeyVisible("kubernetes_namespace_name");
await pm.dashboardCellExplorer.expectKvKeyHidden("_timestamp");
testLogger.info("KV table search narrows the field list to matching keys");
await pm.dashboardCellExplorer.closeDrawer();
await cleanupTestDashboard(page, pm, dashboardName);
});
});