mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 00:08:55 +00:00
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:
co-authored by
Shrinath-O2
ktx-akshay
Neha P
parent
930b534d7b
commit
0b1e67bc2c
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user