test(autoe2e): generated E2E tests for #14492 (dashboard-print-layout) (#14602)

Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com>
Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
This commit is contained in:
Shrinath Rao
2026-09-23 08:18:33 +00:00
committed by GitHub
co-authored by Shrinath-O2 ktx-akshay
parent 18961f7c59
commit 20ddfa4e8f
6 changed files with 437 additions and 2 deletions
+2 -1
View File
@@ -99,7 +99,8 @@
"dashboard-multi-sql.spec.js",
"crossLinkMultiStream.spec.js",
"dashboards-form-validation.spec.js",
"dashboard-refresh-without-cache.spec.js"
"dashboard-refresh-without-cache.spec.js",
"dashboard-print-layout.spec.js"
],
"disabled": [
{
+68
View File
@@ -377,6 +377,74 @@ class APICleanup {
return { dashboardId, folderId };
}
/**
* Create a dashboard with `panelCount` stacked bar panels, each laid out at
* full width (w:24, h:8) and offset vertically (y: 0, 9, 18, ...) so the grid
* is tall enough to exceed one print page. Used by the dashboard print-layout
* specs, which need a deterministic multi-panel grid geometry.
* @param {string} title - Dashboard title
* @param {number} [panelCount=4] - Number of stacked panels
* @param {string} [streamName='e2e_automate'] - Logs stream each panel queries
* @returns {Promise<{dashboardId: string, folderId: string}>}
*/
async createDashboardWithStackedPanels(title, panelCount = 4, streamName = 'e2e_automate') {
const panels = [];
for (let i = 0; i < panelCount; i++) {
panels.push({
id: `Panel_ID${Date.now()}_${i}`,
type: 'bar',
title: `${title} Panel ${i + 1}`,
description: '',
config: { show_legends: false, decimals: 2, drilldown: [] },
queryType: 'sql',
queries: [
{
query: `SELECT histogram(_timestamp) as "x_axis_1", count(_timestamp) as "y_axis_1" FROM "${streamName}" GROUP BY x_axis_1`,
vrlFunctionQuery: '',
customQuery: false,
fields: {
stream: streamName,
stream_type: 'logs',
x: [{ label: 'Timestamp', alias: 'x_axis_1', column: '_timestamp', color: null, aggregationFunction: 'histogram' }],
y: [{ label: 'Count', alias: 'y_axis_1', column: '_timestamp', color: '#5960b2', aggregationFunction: 'count' }],
z: [],
breakdown: [],
filter: { filterType: 'group', logicalOperator: 'AND', conditions: [] },
},
config: { promql_legend: '', layer_type: 'scatter', weight_fixed: 1, limit: 0, min: 0, max: 100 },
},
],
layout: { x: 0, y: i * 9, w: 24, h: 8, i: i + 1 },
});
}
const payload = {
version: 5,
title,
description: '',
role: '',
owner: this.email,
tabs: [{ tabId: 'default', name: 'Default', panels }],
variables: {},
};
const response = await this._fetch(
`${this.baseUrl}/api/${this.org}/dashboards?folder=${encodeURIComponent('default')}`,
{
method: 'POST',
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
);
if (!response.ok) {
const body = await response.text();
throw new Error(`createDashboardWithStackedPanels: HTTP ${response.status} — ${body}`);
}
const result = await response.json();
const inner = result[`v${result.version}`] || result;
const dashboardId = inner.dashboardId || inner.dashboard_id || result.dashboard_id || result.id;
testLogger.info('Created dashboard with stacked panels', { dashboardId, panelCount, folderId: 'default' });
return { dashboardId, folderId: 'default' };
}
/**
* Delete a single dashboard
* @param {string} dashboardId - The dashboard ID
@@ -0,0 +1,184 @@
// Dashboard Print Layout page object — print mode entry/exit + layout state.
const { expect } = require("@playwright/test");
export default class DashboardPrintPage {
constructor(page) {
this.page = page;
this.printBtn = page.locator('[data-test="dashboard-print-btn"]');
this.printModeContainer = page.locator(".print-mode-container");
this.backBtn = page.locator('[data-test="dashboard-back-btn"]');
this.refreshBtn = page.locator('[data-test="dashboard-refresh-btn"]');
this.gridStack = page.locator(".grid-stack");
this.panelContainer = page.locator('[data-test="dashboard-panel-container"]');
this.tablePanel = page.locator(
'[data-test="dashboard-panel-container"][data-panel-type="table"]'
);
this.printPageStyle = page.locator("#o2-print-page");
this.captureReadySpan = page.locator("#dashboardVariablesAndPanelsDataLoaded");
this.emptyStateArt = page.locator('[data-test="empty-panel-art"]');
this.emptyStateAddPanelBtn = page.locator(
'[data-test="dashboard-if-no-panel-add-panel-btn"]'
);
}
async navigateToDashboardView(dashboardId, folderId, print = false) {
let url = `${process.env.ZO_BASE_URL}/web/dashboards/view?org_identifier=${process.env.ORGNAME}&dashboard=${dashboardId}&folder=${folderId}`;
if (print) url += "&print=true";
await this.page.goto(url, { waitUntil: "domcontentloaded" });
}
async clickPrintButton() {
await this.printBtn.waitFor({ state: "visible", timeout: 30000 });
await this.printBtn.click();
}
// Wait for the first panel to mount (grid is laid out with a real width).
async waitForPanelMounted(timeout = 30000) {
await this.panelContainer.first().waitFor({ state: "visible", timeout });
}
// Multi-panel print layout: injected @page style + non-empty grid height +
// every panel force-mounted (no placeholders left).
async waitForPrintLayout(timeout = 30000) {
await expect
.poll(
() =>
this.page.evaluate(() => {
const style = document.getElementById("o2-print-page");
const grid = document.querySelector(".grid-stack");
const placeholders = document.querySelectorAll(
'[data-test^="dashboard-panel-placeholder-"]'
).length;
const height = grid ? (grid.style.height || "").trim() : "";
return (
!!style &&
(style.textContent || "").includes("@page") &&
height !== "" &&
placeholders === 0
);
}),
{ timeout, intervals: [500, 1000, 2000] }
)
.toBe(true);
}
async enterPrintMode() {
await this.clickPrintButton();
await this.waitForPrintLayout();
}
// Single-table print branch: no grid, one full-width table panel, print chrome on.
async waitForSingleTablePrintMode(timeout = 30000) {
await expect
.poll(
() =>
this.page.evaluate(() => {
const gridCount = document.querySelectorAll(".grid-stack").length;
const tableCount = document.querySelectorAll(
'[data-test="dashboard-panel-container"][data-panel-type="table"]'
).length;
const containerCount = document.querySelectorAll(
".print-mode-container"
).length;
return gridCount === 0 && tableCount === 1 && containerCount === 1;
}),
{ timeout, intervals: [500, 1000, 2000] }
)
.toBe(true);
}
// Print layout cleared = px top overrides restored to calc(). GridStack
// re-initializes on exit and re-sets a natural (non-empty) inline height, so
// the print override is measured via panel tops, not an empty grid height.
async waitForPrintModeCleared(timeout = 30000) {
await expect
.poll(
() =>
this.page.evaluate(() => {
const style = document.getElementById("o2-print-page");
const containerCount = document.querySelectorAll(
".print-mode-container"
).length;
const overrideCount = Array.from(
document.querySelectorAll(".grid-stack-item")
).filter((el) => /px$/.test((el.style.top || "").trim())).length;
return !style && containerCount === 0 && overrideCount === 0;
}),
{ timeout, intervals: [500, 1000, 2000] }
)
.toBe(true);
}
async exitPrintMode() {
await this.clickPrintButton();
await this.waitForPrintModeCleared();
}
// The capture-readiness span is `display:none`, so it is asserted as attached.
async waitForCaptureReady(timeout = 60000) {
await expect(this.captureReadySpan).toBeAttached({ timeout });
}
async getGridStackInlineHeight() {
return this.page.evaluate(() => {
const grid = document.querySelector(".grid-stack");
return grid ? (grid.style.height || "").trim() : "";
});
}
// preparePrintLayout overrides every panel top with a plain `px` value; before
// print mode GridStack positions items with a calc() formula, so px count > 0
// proves the reflow ran.
async getPanelTopOverrideCount() {
return this.page.evaluate(() => {
return Array.from(document.querySelectorAll(".grid-stack-item")).filter((el) =>
/px$/.test((el.style.top || "").trim())
).length;
});
}
// ----- assertions -----
async expectPrintChromeApplied() {
await expect(this.printModeContainer).toHaveCount(1);
await expect(this.backBtn).toBeHidden();
await expect(this.refreshBtn).toBeHidden();
}
async expectPrintChromeCleared() {
await expect(this.printModeContainer).toHaveCount(0);
await expect(this.backBtn).toBeVisible();
await expect(this.refreshBtn).toBeVisible();
}
async expectPrintModeContainerVisible() {
await expect(this.printModeContainer).toBeVisible();
}
async expectPrintPageStyleInjected() {
await expect(this.printPageStyle).toHaveCount(1);
const text = await this.printPageStyle.textContent();
expect(text).toContain("@page");
}
async expectPrintPageStyleRemoved() {
await expect(this.printPageStyle).toHaveCount(0);
}
async expectGridStackAbsent() {
await expect(this.gridStack).toHaveCount(0);
}
async expectSingleTableFullWidth() {
await expect(this.tablePanel).toHaveCount(1);
await expect(this.tablePanel).toBeVisible();
}
async expectEmptyStateRendered() {
await expect(this.emptyStateArt).toBeVisible();
}
async expectEmptyStateAddPanelBtnHidden() {
await expect(this.emptyStateAddPanelBtn).toHaveCount(0);
}
}
+2
View File
@@ -20,6 +20,7 @@ import DashboardLegendsCopy from "./dashboardPages/dashboard-legends-copy";
import DashboardFilter from "./dashboardPages/dashboard-filter";
import DashboardImport from "./dashboardPages/dashboard-import.js";
import DashboardShareExportPage from "./dashboardPages/dashboard-share-export";
import DashboardPrintPage from "./dashboardPages/dashboard-print";
import DashboardTimeRefresh from "./dashboardPages/dashboard-refresh";
import DateTimeHelper from "./dashboardPages/dashboard-time";
import DashboardPanelTime from "./dashboardPages/dashboard-panel-time";
@@ -156,6 +157,7 @@ class PageManager {
this.dashboardFilter = new DashboardFilter(page);
this.dashboardImport = new DashboardImport(page);
this.dashboardShareExport = new DashboardShareExportPage(page);
this.dashboardPrint = new DashboardPrintPage(page);
this.dashboardTimeRefresh = new DashboardTimeRefresh(page);
this.dateTimeHelper = new DateTimeHelper(page);
this.dashboardPanelTime = new DashboardPanelTime(page);
@@ -0,0 +1,179 @@
import * as crypto from "crypto";
import { test, expect, navigateToBase } from "../utils/enhanced-baseFixtures.js";
import testLogger from "../utils/test-logger.js";
import PageManager from "../../pages/page-manager.js";
import { ingestion } from "./utils/dashIngestion.js";
import { setupTestDashboard, deleteDashboard } from "./utils/dashCreation.js";
import {
generateDashboardName,
setupTablePanel,
} from "./utils/configPanelHelpers.js";
test.describe("Dashboard Print Layout testcases", () => {
test.describe.configure({ mode: "parallel" });
let pm;
let apiDashboard = null;
let uiDashboardName = null;
test.beforeEach(async ({ page }, testInfo) => {
testLogger.testStart(testInfo.title, testInfo.file);
apiDashboard = null;
uiDashboardName = null;
await navigateToBase(page);
pm = new PageManager(page);
await ingestion(page);
testLogger.info("Test setup completed");
});
test.afterEach(async ({ page }) => {
if (apiDashboard) {
await pm.apiCleanup
.deleteDashboard(apiDashboard.dashboardId, apiDashboard.folderId)
.catch(() => {});
apiDashboard = null;
}
if (uiDashboardName) {
try {
await page.goto(
`${process.env["ZO_BASE_URL"]}/web/dashboards?org_identifier=${process.env["ORGNAME"]}`,
{ waitUntil: "domcontentloaded" }
);
await deleteDashboard(page, uiDashboardName);
} catch (e) {
testLogger.warn("UI dashboard cleanup failed (non-fatal)", { error: e.message });
}
uiDashboardName = null;
}
});
test(
"should enter print mode via the toolbar button and render the print layout",
{ tag: ["@dashboard-print-layout", "@all", "@P0"] },
async ({ page }) => {
const title = `PrintLayout_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
apiDashboard = { dashboardId, folderId };
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId);
await pm.dashboardPrint.waitForPanelMounted();
await pm.dashboardPrint.enterPrintMode();
await expect(page).toHaveURL(/print=true/);
await pm.dashboardPrint.expectPrintChromeApplied();
await pm.dashboardPrint.expectPrintPageStyleInjected();
const gridHeight = await pm.dashboardPrint.getGridStackInlineHeight();
expect(gridHeight).not.toBe("");
const overrideCount = await pm.dashboardPrint.getPanelTopOverrideCount();
expect(overrideCount).toBeGreaterThan(0);
testLogger.info("Print layout rendered", { gridHeight, overrideCount });
}
);
test(
"should exit print mode and clear the print layout",
{ tag: ["@dashboard-print-layout", "@all", "@P0"] },
async ({ page }) => {
const title = `PrintLayoutExit_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
apiDashboard = { dashboardId, folderId };
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId);
await pm.dashboardPrint.waitForPanelMounted();
await pm.dashboardPrint.enterPrintMode();
await pm.dashboardPrint.exitPrintMode();
await expect(page).not.toHaveURL(/print=true/);
await pm.dashboardPrint.expectPrintPageStyleRemoved();
await pm.dashboardPrint.expectPrintChromeCleared();
const overrideCount = await pm.dashboardPrint.getPanelTopOverrideCount();
expect(overrideCount).toBe(0);
testLogger.info("Print layout cleared on exit");
}
);
test(
"should enter print mode via the ?print=true URL and persist across reload",
{ tag: ["@dashboard-print-layout", "@all", "@P1"] },
async ({ page }) => {
const title = `PrintLayoutUrl_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
apiDashboard = { dashboardId, folderId };
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId, true);
await pm.dashboardPrint.waitForPrintLayout();
await pm.dashboardPrint.expectPrintChromeApplied();
await pm.dashboardPrint.expectPrintPageStyleInjected();
await page.reload();
await pm.dashboardPrint.waitForPrintLayout();
await pm.dashboardPrint.expectPrintChromeApplied();
testLogger.info("Print mode persisted across reload");
}
);
test(
"should render a single-table-panel dashboard full-width in print mode",
{ tag: ["@dashboard-print-layout", "@all", "@P1"] },
async ({ page }) => {
const dashboardName = generateDashboardName();
uiDashboardName = dashboardName;
await setupTablePanel(page, pm, dashboardName);
await pm.dashboardPanelActions.savePanel();
await pm.dashboardPrint.clickPrintButton();
await pm.dashboardPrint.waitForSingleTablePrintMode();
await pm.dashboardPrint.expectGridStackAbsent();
await pm.dashboardPrint.expectSingleTableFullWidth();
await pm.dashboardPrint.expectPrintModeContainerVisible();
testLogger.info("Single-table full-width print branch verified");
}
);
test(
"should show the print-capture readiness flag once panels finish loading",
{ tag: ["@dashboard-print-layout", "@all", "@P1"] },
async ({ page }) => {
const title = `PrintLayoutReady_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
apiDashboard = { dashboardId, folderId };
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId);
await pm.dashboardPrint.waitForPanelMounted();
await pm.dashboardPrint.enterPrintMode();
await pm.dashboardPrint.waitForCaptureReady();
testLogger.info("Print-capture readiness flag appeared");
}
);
test(
"should inject no print page style for an empty dashboard in print mode",
{ tag: ["@dashboard-print-layout", "@all", "@P2"] },
async ({ page }) => {
const dashboardName = generateDashboardName();
uiDashboardName = dashboardName;
await setupTestDashboard(page, pm, dashboardName);
await pm.dashboardPrint.clickPrintButton();
await pm.dashboardPrint.expectPrintModeContainerVisible();
await pm.dashboardPrint.expectPrintPageStyleRemoved();
await pm.dashboardPrint.expectGridStackAbsent();
await pm.dashboardPrint.expectEmptyStateRendered();
await pm.dashboardPrint.expectEmptyStateAddPanelBtnHidden();
testLogger.info("Empty dashboard in print mode verified");
}
);
});
@@ -4,6 +4,7 @@
* and (where indicated) opens the config sidebar, ready for assertions.
*/
import * as crypto from "crypto";
import { setupTestDashboard, deleteDashboard } from "./dashCreation.js";
import { ingestionForMaps } from "./dashIngestion.js";
import testLogger from '../../utils/test-logger.js';
@@ -81,7 +82,7 @@ export async function discardAndCleanupTestDashboard(page, dashboardName) {
}
export const generateDashboardName = () =>
"Dashboard_" + Math.random().toString(36).slice(2, 11) + "_" + Date.now();
"Dashboard_" + crypto.randomBytes(6).toString("hex") + "_" + Date.now();
// ---------------------------------------------------------------------------
// Base panel builder — all other helpers delegate here