From 20ddfa4e8f187c44d6d95cc8450bf7535d32f73c Mon Sep 17 00:00:00 2001 From: Shrinath Rao Date: Wed, 23 Sep 2026 08:18:33 +0000 Subject: [PATCH] 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 --- tests/ui-testing/ci-matrix/ci_matrix.json | 3 +- tests/ui-testing/pages/apiCleanup.js | 68 +++++++ .../pages/dashboardPages/dashboard-print.js | 184 ++++++++++++++++++ tests/ui-testing/pages/page-manager.js | 2 + .../Dashboards/dashboard-print-layout.spec.js | 179 +++++++++++++++++ .../Dashboards/utils/configPanelHelpers.js | 3 +- 6 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 tests/ui-testing/pages/dashboardPages/dashboard-print.js create mode 100644 tests/ui-testing/playwright-tests/Dashboards/dashboard-print-layout.spec.js diff --git a/tests/ui-testing/ci-matrix/ci_matrix.json b/tests/ui-testing/ci-matrix/ci_matrix.json index 547b442f97..c31ca0e735 100644 --- a/tests/ui-testing/ci-matrix/ci_matrix.json +++ b/tests/ui-testing/ci-matrix/ci_matrix.json @@ -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": [ { diff --git a/tests/ui-testing/pages/apiCleanup.js b/tests/ui-testing/pages/apiCleanup.js index c274e050d9..5a973c3e65 100644 --- a/tests/ui-testing/pages/apiCleanup.js +++ b/tests/ui-testing/pages/apiCleanup.js @@ -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 diff --git a/tests/ui-testing/pages/dashboardPages/dashboard-print.js b/tests/ui-testing/pages/dashboardPages/dashboard-print.js new file mode 100644 index 0000000000..62d9f13d0d --- /dev/null +++ b/tests/ui-testing/pages/dashboardPages/dashboard-print.js @@ -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); + } +} diff --git a/tests/ui-testing/pages/page-manager.js b/tests/ui-testing/pages/page-manager.js index d2a79261bd..b47be8fb3d 100644 --- a/tests/ui-testing/pages/page-manager.js +++ b/tests/ui-testing/pages/page-manager.js @@ -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); diff --git a/tests/ui-testing/playwright-tests/Dashboards/dashboard-print-layout.spec.js b/tests/ui-testing/playwright-tests/Dashboards/dashboard-print-layout.spec.js new file mode 100644 index 0000000000..1b3b6628ee --- /dev/null +++ b/tests/ui-testing/playwright-tests/Dashboards/dashboard-print-layout.spec.js @@ -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"); + } + ); +}); diff --git a/tests/ui-testing/playwright-tests/Dashboards/utils/configPanelHelpers.js b/tests/ui-testing/playwright-tests/Dashboards/utils/configPanelHelpers.js index 482cf92783..aff79351a1 100644 --- a/tests/ui-testing/playwright-tests/Dashboards/utils/configPanelHelpers.js +++ b/tests/ui-testing/playwright-tests/Dashboards/utils/configPanelHelpers.js @@ -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