diff --git a/tests/ui-testing/playwright-tests/Dashboards/dashboard-config-drilldown.spec.js b/tests/ui-testing/playwright-tests/Dashboards/dashboard-config-drilldown.spec.js index f47ecabb14..c69d8a7e0a 100644 --- a/tests/ui-testing/playwright-tests/Dashboards/dashboard-config-drilldown.spec.js +++ b/tests/ui-testing/playwright-tests/Dashboards/dashboard-config-drilldown.spec.js @@ -5,7 +5,7 @@ const { } = require("../utils/enhanced-baseFixtures.js"); import PageManager from "../../pages/page-manager"; import { ingestion } from "./utils/dashIngestion.js"; -import { cleanupTestDashboard, deleteDashboard } from "./utils/dashCreation.js"; +import { cleanupTestDashboard, deleteDashboard, waitForDashboardPage } from "./utils/dashCreation.js"; import { generateDashboardName, setupBarPanelWithConfig, @@ -13,6 +13,13 @@ import { setupDestinationDashboardWithTabs, reopenPanelConfig, } from "./utils/configPanelHelpers.js"; +import DashboardVariablesScoped from "../../pages/dashboardPages/dashboard-variables-scoped.js"; +const { safeWaitForHidden, safeWaitForNetworkIdle } = require("../utils/wait-helpers.js"); +const { + SELECTORS, + getVariableSelector, + getEditVariableBtn, +} = require("../../pages/dashboardPages/dashboard-selectors.js"); const testLogger = require('../utils/test-logger.js'); test.describe.configure({ mode: "parallel" }); @@ -513,3 +520,606 @@ test.describe("ConfigPanel — Drilldown Configuration", () => { await deleteDashboard(page, destinationDashName); }); }); + +// ═══════════════════════════════════════════════════════════════════════════════ +// Same-Dashboard Drilldown — URL Clobbering Regression (PR #11134) +// +// These tests cover the exact regression fixed by PR #11134: +// "fix: enhance drilldown handling to prevent URL clobbering during same +// dashboard navigation" +// +// Bug: when a drilldown targeted the same dashboard, var-* URL params injected +// by the drilldown were immediately overwritten by updateUrlWithCurrentState() +// before the variables manager could process the new values. +// +// Fix: two guard flags (isDrilldownInProgress, isInternalUrlUpdate) and a new +// var-* watcher in ViewDashboard.vue prevent the clobber and update variables +// in-place without a full reload. +// ═══════════════════════════════════════════════════════════════════════════════ + +test.describe( + "Same-Dashboard Drilldown — URL Clobbering Regression (PR #11134)", + { + tag: [ + "@dashboards", + "@drilldown", + "@urlSync", + "@regression", + "@P1", + ], + }, + () => { + test.describe.configure({ mode: "serial" }); + + test.beforeEach(async ({ page }) => { + await navigateToBase(page); + await ingestion(page); + }); + + // ───────────────────────────────────────────────────────────────────────── + // Test 15: Same-dashboard drilldown with custom variable mapping + // Verifies that a var-* value injected by a byDashboard drilldown (same dash) + // is NOT overwritten by URL sync after the navigation settles. + // ───────────────────────────────────────────────────────────────────────── + test( + "same-dashboard drilldown with custom variable: var-* param present in URL after click and not overwritten by URL sync", + async ({ page }) => { + testLogger.testStart( + "same-dashboard drilldown: custom variable — no URL clobber" + ); + + const pm = new PageManager(page); + const scopedVars = new DashboardVariablesScoped(page); + const dashboardName = `Dashboard_SameDash_Custom_${Date.now()}`; + const variableName = `sd_var_${Date.now()}`; + const drilldownVarValue = "kube-system"; + + // ── 1. Create dashboard ────────────────────────────────────────────── + await pm.dashboardList.menuItem("dashboards-item"); + await waitForDashboardPage(page); + await pm.dashboardCreate.waitForDashboardUIStable(); + await pm.dashboardCreate.createDashboard(dashboardName); + await page + .locator(SELECTORS.ADD_PANEL_BTN) + .waitFor({ state: "visible" }); + + // ── 2. Add global variable ─────────────────────────────────────────── + await pm.dashboardSetting.openSetting(); + await scopedVars.addScopedVariable( + variableName, + "logs", + "e2e_automate", + "kubernetes_namespace_name", + { scope: "global" } + ); + await page + .locator(getEditVariableBtn(variableName)) + .waitFor({ state: "visible", timeout: 10000 }); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + await pm.dashboardSetting.closeSettingWindow(); + await safeWaitForHidden(page, '[data-test="dashboard-settings-dialog"]', { timeout: 5000 }); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + testLogger.info(`Variable '${variableName}' created`); + + // ── 3. Add table panel ─────────────────────────────────────────────── + await pm.dashboardCreate.addPanel(); + await pm.chartTypeSelector.selectChartType("table"); + await pm.chartTypeSelector.selectStreamType("logs"); + await pm.chartTypeSelector.selectStream("e2e_automate"); + await pm.chartTypeSelector.searchAndAddField( + "kubernetes_container_hash", + "y" + ); + await pm.dashboardPanelActions.addPanelName("Source Panel"); + await pm.dashboardPanelActions.applyDashboardBtn(); + await pm.dashboardPanelActions.waitForChartToRender(); + + // ── 4. Configure drilldown: same dashboard + custom variable mapping ─ + await pm.dashboardPanelConfigs.openConfigPanel(); + + // Open drilldown popup + const addBtn = page + .locator('[data-test="dashboard-addpanel-config-drilldown-add-btn"]') + .first(); + await addBtn.scrollIntoViewIfNeeded(); + await addBtn.click(); + const popup = page.locator('[data-test="dashboard-drilldown-popup"]'); + await popup.waitFor({ state: "visible", timeout: 10000 }); + + // Fill drilldown name + await page + .locator('[data-test="dashboard-config-panel-drilldown-name"]') + .fill("Same Dash Custom Var"); + + // byDashboard is the default type — select folder → same dashboard → tab + await page + .locator('[data-test="dashboard-drilldown-folder-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-folder-select"]') + .click(); + await page + .getByRole("option", { name: "default", exact: true }) + .click(); + + await page + .locator('[data-test="dashboard-drilldown-dashboard-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-dashboard-select"]') + .click(); + await page + .getByRole("option", { name: dashboardName, exact: true }) + .click(); + + await page + .locator('[data-test="dashboard-drilldown-tab-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-tab-select"]') + .click(); + await page.getByRole("option").first().click(); + testLogger.info("Drilldown target: same dashboard selected"); + + // Add variable mapping: variableName → drilldownVarValue (literal value) + const addVarBtn = page.locator( + '[data-test="dashboard-drilldown-add-variable"]' + ); + await addVarBtn.waitFor({ state: "visible", timeout: 5000 }); + await addVarBtn.click(); + await page.waitForTimeout(300); + + // Fill variable name and value using CommonAutoComplete inputs inside popup. + // In Quasar v2, data-test on is applied to the native + // element via $attrs, so the selector targets the input directly. + const autoCompleteInputs = popup.locator( + '[data-test="common-auto-complete"]' + ); + await autoCompleteInputs.first().waitFor({ state: "visible", timeout: 5000 }); + await autoCompleteInputs.first().scrollIntoViewIfNeeded(); + await autoCompleteInputs.first().fill(variableName); + await autoCompleteInputs.last().fill(drilldownVarValue); + testLogger.info( + `Variable mapping: ${variableName} → ${drilldownVarValue}` + ); + + // Confirm and close popup + await page + .locator('[data-test="confirm-button"]') + .waitFor({ state: "visible", timeout: 5000 }); + await page.locator('[data-test="confirm-button"]').click(); + await popup.waitFor({ state: "hidden", timeout: 10000 }); + + // ── 5. Save panel ──────────────────────────────────────────────────── + await pm.dashboardPanelActions.applyDashboardBtn(); + await pm.dashboardPanelActions.waitForChartToRender(); + await pm.dashboardPanelActions.savePanel(); + testLogger.info("Panel saved — now on dashboard view"); + + // ── 6. Wait for panel to render on view page ───────────────────────── + await page + .locator('[data-test="dashboard-panel-table"]') + .first() + .waitFor({ state: "attached", timeout: 20000 }); + await safeWaitForNetworkIdle(page, { timeout: 5000 }); + + // ── 7. Trigger same-dashboard drilldown via table row click ─────────── + testLogger.info("Triggering same-dashboard drilldown from table row"); + const drilldownMenu = + await pm.dashboardDrilldown.triggerDrilldownFromTable(); + await expect(drilldownMenu).toBeVisible({ timeout: 5000 }); + await expect( + pm.dashboardDrilldown.drilldownMenuFirstItem + ).toContainText("Same Dash Custom Var"); + + await pm.dashboardDrilldown.drilldownMenuFirstItem.click(); + + try { + // Wait for var-* param to appear — waitForURL(/\/dashboards\/view/) is a + // no-op for same-dashboard drilldowns (page is already on that path). + await page.waitForURL( + u => new URL(u).searchParams.has(`var-${variableName}`), + { timeout: 15000 } + ); + + // ── 8. Assert var-* VALUE immediately after drilldown ───────────── + // Asserting key+value (not just key existence) catches a clobber that + // rewrites to var-X=oldValue, which a .toContain("var-X=") check misses. + const varValueAfterDrilldown = new URL(page.url()).searchParams.get(`var-${variableName}`); + testLogger.info(`URL after drilldown: ${page.url()}`); + expect(varValueAfterDrilldown).toBeTruthy(); + testLogger.info(`✅ var-${variableName}=${varValueAfterDrilldown} present in URL after drilldown`); + + // ── 9. Regression guard: same value must survive 800ms ──────────── + // Before PR #11134 fix: updateUrlWithCurrentState() would rewrite var-* + // back to the committed (stale) value — the value would change or vanish. + await page.waitForTimeout(800); + const varValueAfterWait = new URL(page.url()).searchParams.get(`var-${variableName}`); + testLogger.info(`URL after 800ms wait: ${page.url()}`); + expect(varValueAfterWait).toBe(varValueAfterDrilldown); + testLogger.info( + `✅ URL clobber guard passed — var-${variableName} value unchanged 800ms after same-dashboard drilldown` + ); + } finally { + // ── Cleanup — runs even if assertions above throw ───────────────── + await pm.dashboardList.menuItem("dashboards-item").catch(() => {}); + await deleteDashboard(page, dashboardName).catch(() => {}); + } + } + ); + + // ───────────────────────────────────────────────────────────────────────── + // Test 16: Same-dashboard drilldown with passAllVariables + // When passAllVariables = true, the current variable value should be + // carried through and persist in URL after same-dashboard navigation. + // ───────────────────────────────────────────────────────────────────────── + test( + "same-dashboard drilldown with passAllVariables: current var-* value carried through URL and not overwritten", + async ({ page }) => { + testLogger.testStart( + "same-dashboard drilldown: passAllVariables — var persists" + ); + + const pm = new PageManager(page); + const scopedVars = new DashboardVariablesScoped(page); + const dashboardName = `Dashboard_SameDash_PassAll_${Date.now()}`; + const variableName = `pav_var_${Date.now()}`; + + // ── 1. Create dashboard ────────────────────────────────────────────── + await pm.dashboardList.menuItem("dashboards-item"); + await waitForDashboardPage(page); + await pm.dashboardCreate.waitForDashboardUIStable(); + await pm.dashboardCreate.createDashboard(dashboardName); + await page + .locator(SELECTORS.ADD_PANEL_BTN) + .waitFor({ state: "visible" }); + + // ── 2. Add global variable ─────────────────────────────────────────── + await pm.dashboardSetting.openSetting(); + await scopedVars.addScopedVariable( + variableName, + "logs", + "e2e_automate", + "kubernetes_namespace_name", + { scope: "global" } + ); + await page + .locator(getEditVariableBtn(variableName)) + .waitFor({ state: "visible", timeout: 10000 }); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + await pm.dashboardSetting.closeSettingWindow(); + await safeWaitForHidden(page, '[data-test="dashboard-settings-dialog"]', { timeout: 5000 }); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + + // ── 3. Add table panel with byDashboard drilldown, passAllVariables ── + await pm.dashboardCreate.addPanel(); + await pm.chartTypeSelector.selectChartType("table"); + await pm.chartTypeSelector.selectStreamType("logs"); + await pm.chartTypeSelector.selectStream("e2e_automate"); + await pm.chartTypeSelector.searchAndAddField( + "kubernetes_container_hash", + "y" + ); + await pm.dashboardPanelActions.addPanelName("PassAll Panel"); + await pm.dashboardPanelActions.applyDashboardBtn(); + await pm.dashboardPanelActions.waitForChartToRender(); + + await pm.dashboardPanelConfigs.openConfigPanel(); + + // Open drilldown popup + const addBtn = page + .locator('[data-test="dashboard-addpanel-config-drilldown-add-btn"]') + .first(); + await addBtn.scrollIntoViewIfNeeded(); + await addBtn.click(); + const popup = page.locator('[data-test="dashboard-drilldown-popup"]'); + await popup.waitFor({ state: "visible", timeout: 10000 }); + + await page + .locator('[data-test="dashboard-config-panel-drilldown-name"]') + .fill("PassAll Same Dash"); + + // Target: same dashboard + await page + .locator('[data-test="dashboard-drilldown-folder-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-folder-select"]') + .click(); + await page + .getByRole("option", { name: "default", exact: true }) + .click(); + + await page + .locator('[data-test="dashboard-drilldown-dashboard-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-dashboard-select"]') + .click(); + await page + .getByRole("option", { name: dashboardName, exact: true }) + .click(); + + await page + .locator('[data-test="dashboard-drilldown-tab-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-tab-select"]') + .click(); + await page.getByRole("option").first().click(); + + // Enable passAllVariables + const passAllToggle = page.locator( + '[data-test="dashboard-drilldown-pass-all-variables"]' + ); + await passAllToggle.waitFor({ state: "visible", timeout: 5000 }); + await passAllToggle.click(); + testLogger.info("passAllVariables enabled"); + + await page.locator('[data-test="confirm-button"]').click(); + await popup.waitFor({ state: "hidden", timeout: 10000 }); + + await pm.dashboardPanelActions.applyDashboardBtn(); + await pm.dashboardPanelActions.waitForChartToRender(); + await pm.dashboardPanelActions.savePanel(); + testLogger.info("Panel with passAllVariables drilldown saved"); + + // ── 4. On dashboard view: select a variable value ──────────────────── + await page + .locator(getVariableSelector(variableName)) + .waitFor({ state: "visible", timeout: 10000 }); + const { selectedValue } = await scopedVars.changeVariableValue( + variableName, + { optionIndex: 0, returnSelectedValue: true } + ); + testLogger.info(`Selected variable value: "${selectedValue}"`); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + + // Confirm var-* is in URL (normal variable sync) + const urlBeforeDrilldown = page.url(); + expect(urlBeforeDrilldown).toContain(`var-${variableName}=`); + testLogger.info("var-* confirmed in URL before drilldown"); + + // ── 5. Trigger same-dashboard drilldown ────────────────────────────── + const drilldownMenu = + await pm.dashboardDrilldown.triggerDrilldownFromTable(); + await expect(drilldownMenu).toBeVisible({ timeout: 5000 }); + await expect( + pm.dashboardDrilldown.drilldownMenuFirstItem + ).toContainText("PassAll Same Dash"); + + await pm.dashboardDrilldown.drilldownMenuFirstItem.click(); + + try { + // Wait for var-* param to appear — no-op waitForURL(/view/) replaced with + // a param-level wait so we don't race the async URL update. + await page.waitForURL( + u => new URL(u).searchParams.has(`var-${variableName}`), + { timeout: 15000 } + ); + + // ── 6. Assert var-* VALUE matches what was selected ─────────────── + // selectedValue was captured above but previously unused — asserting it + // here means a clobber back to an empty/stale value will fail the test. + const varValueAfterDrilldown = decodeURIComponent( + new URL(page.url()).searchParams.get(`var-${variableName}`) + ); + testLogger.info(`URL after drilldown: ${page.url()}`); + expect(varValueAfterDrilldown).toBe(selectedValue); + testLogger.info(`✅ var-${variableName}=${varValueAfterDrilldown} matches selected value`); + + // Regression guard: same value must survive 800ms + await page.waitForTimeout(800); + const varValueAfterWait = decodeURIComponent( + new URL(page.url()).searchParams.get(`var-${variableName}`) + ); + expect(varValueAfterWait).toBe(selectedValue); + testLogger.info( + `✅ passAllVariables: var-${variableName} value unchanged 800ms after same-dashboard drilldown` + ); + } finally { + // ── Cleanup — runs even if assertions above throw ───────────────── + await pm.dashboardList.menuItem("dashboards-item").catch(() => {}); + await deleteDashboard(page, dashboardName).catch(() => {}); + } + } + ); + + // ───────────────────────────────────────────────────────────────────────── + // Test 17: Same-dashboard drilldown targeting a different tab + // Dashboard ID stays the same, but tab changes + var-* params are passed. + // Verifies the var-* watcher handles tab-switch within same-dashboard case. + // ───────────────────────────────────────────────────────────────────────── + test( + "same-dashboard drilldown to a different tab: tab changes + var-* params present and not clobbered", + async ({ page }) => { + testLogger.testStart( + "same-dashboard drilldown: different tab — var-* and tab persist" + ); + + const pm = new PageManager(page); + const scopedVars = new DashboardVariablesScoped(page); + const dashboardName = `Dashboard_SameDash_Tab_${Date.now()}`; + const variableName = `tab_var_${Date.now()}`; + const secondTabName = "Analytics Tab"; + + // ── 1. Create dashboard with a second tab and a global variable ─────── + await pm.dashboardList.menuItem("dashboards-item"); + await waitForDashboardPage(page); + await pm.dashboardCreate.waitForDashboardUIStable(); + await pm.dashboardCreate.createDashboard(dashboardName); + await page + .locator(SELECTORS.ADD_PANEL_BTN) + .waitFor({ state: "visible" }); + + // Add global variable (open/close settings session) + await pm.dashboardSetting.openSetting(); + await scopedVars.addScopedVariable( + variableName, + "logs", + "e2e_automate", + "kubernetes_namespace_name", + { scope: "global" } + ); + await page + .locator(getEditVariableBtn(variableName)) + .waitFor({ state: "visible", timeout: 10000 }); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + await pm.dashboardSetting.closeSettingWindow(); + await safeWaitForHidden(page, '[data-test="dashboard-settings-dialog"]', { timeout: 5000 }); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + testLogger.info(`Variable '${variableName}' created`); + + // Add second tab in a fresh settings session. + // Use closeSettingDashboard() (not closeSettingWindow()) because addTabAndWait + // leaves the settings dialog open — this matches the pattern in setupDestinationDashboardWithTabs. + await pm.dashboardSetting.openSetting(); + await pm.dashboardSetting.addTabAndWait(secondTabName); + await pm.dashboardSetting.closeSettingDashboard(); + await safeWaitForHidden(page, '[data-test="dashboard-settings-dialog"]', { timeout: 5000 }); + await page.waitForTimeout(400); // allow Quasar fade-out animation to complete + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + testLogger.info(`Second tab '${secondTabName}' added`); + + // ── 2. Add table panel on Default tab with drilldown to same dashboard + second tab ── + await pm.dashboardCreate.addPanel(); + await pm.chartTypeSelector.selectChartType("table"); + await pm.chartTypeSelector.selectStreamType("logs"); + await pm.chartTypeSelector.selectStream("e2e_automate"); + await pm.chartTypeSelector.searchAndAddField( + "kubernetes_container_hash", + "y" + ); + await pm.dashboardPanelActions.addPanelName("Tab Panel"); + await pm.dashboardPanelActions.applyDashboardBtn(); + await pm.dashboardPanelActions.waitForChartToRender(); + + await pm.dashboardPanelConfigs.openConfigPanel(); + + // Open drilldown popup + const addBtn = page + .locator('[data-test="dashboard-addpanel-config-drilldown-add-btn"]') + .first(); + await addBtn.scrollIntoViewIfNeeded(); + await addBtn.click(); + const popup = page.locator('[data-test="dashboard-drilldown-popup"]'); + await popup.waitFor({ state: "visible", timeout: 10000 }); + + await page + .locator('[data-test="dashboard-config-panel-drilldown-name"]') + .fill("Same Dash Tab Drilldown"); + + // Target: same dashboard, second tab + await page + .locator('[data-test="dashboard-drilldown-folder-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-folder-select"]') + .click(); + await page + .getByRole("option", { name: "default", exact: true }) + .click(); + + await page + .locator('[data-test="dashboard-drilldown-dashboard-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-dashboard-select"]') + .click(); + await page + .getByRole("option", { name: dashboardName, exact: true }) + .click(); + + await page + .locator('[data-test="dashboard-drilldown-tab-select"]') + .waitFor({ state: "visible", timeout: 10000 }); + await page + .locator('[data-test="dashboard-drilldown-tab-select"]') + .click(); + await page + .getByRole("option", { name: secondTabName, exact: true }) + .click(); + testLogger.info(`Drilldown target: same dashboard, tab "${secondTabName}"`); + + // Enable passAllVariables to carry var-* through the tab switch + const passAllToggle = page.locator( + '[data-test="dashboard-drilldown-pass-all-variables"]' + ); + await passAllToggle.waitFor({ state: "visible", timeout: 5000 }); + await passAllToggle.click(); + + await page.locator('[data-test="confirm-button"]').click(); + await popup.waitFor({ state: "hidden", timeout: 10000 }); + + await pm.dashboardPanelActions.applyDashboardBtn(); + await pm.dashboardPanelActions.waitForChartToRender(); + await pm.dashboardPanelActions.savePanel(); + testLogger.info("Panel with tab-switch drilldown saved"); + + // ── 3. On dashboard view: select a variable value ──────────────────── + await page + .locator(getVariableSelector(variableName)) + .waitFor({ state: "visible", timeout: 10000 }); + const { selectedValue } = await scopedVars.changeVariableValue( + variableName, + { optionIndex: 0, returnSelectedValue: true } + ); + testLogger.info(`Variable value selected: "${selectedValue}"`); + await safeWaitForNetworkIdle(page, { timeout: 3000 }); + + // Capture dashboard ID from URL (same dashboard should be maintained after drilldown) + const urlBeforeDrilldown = page.url(); + const dashboardParam = new URL(urlBeforeDrilldown).searchParams.get("dashboard"); + testLogger.info(`Dashboard ID before drilldown: ${dashboardParam}`); + + // ── 4. Trigger same-dashboard tab-switch drilldown ─────────────────── + const drilldownMenu = + await pm.dashboardDrilldown.triggerDrilldownFromTable(); + await expect(drilldownMenu).toBeVisible({ timeout: 5000 }); + await expect( + pm.dashboardDrilldown.drilldownMenuFirstItem + ).toContainText("Same Dash Tab Drilldown"); + + await pm.dashboardDrilldown.drilldownMenuFirstItem.click(); + + try { + // Wait for var-* param to appear — param-level wait handles the async + // URL update that a path-level waitForURL would resolve too early. + await page.waitForURL( + u => new URL(u).searchParams.has(`var-${variableName}`), + { timeout: 15000 } + ); + + // ── 5. Assert dashboard ID, var-* VALUE, and tab all correct ──────── + const urlParamsAfter = new URL(page.url()).searchParams; + testLogger.info(`URL after tab-switch drilldown: ${page.url()}`); + + // Dashboard ID must be unchanged (same dashboard, different tab) + expect(urlParamsAfter.get("dashboard")).toBe(dashboardParam); + testLogger.info("✅ Dashboard ID unchanged after same-dashboard drilldown"); + + // var-* value must match what was selected — asserting value (not just + // key) catches a clobber that rewrites to the stale committed value. + const varValueAfterDrilldown = decodeURIComponent( + urlParamsAfter.get(`var-${variableName}`) + ); + expect(varValueAfterDrilldown).toBe(selectedValue); + testLogger.info(`✅ var-${variableName}=${varValueAfterDrilldown} matches selected value`); + + // Regression guard: both dashboard ID and var-* value must survive 800ms + await page.waitForTimeout(800); + const urlParamsAfterWait = new URL(page.url()).searchParams; + expect(decodeURIComponent(urlParamsAfterWait.get(`var-${variableName}`))).toBe(selectedValue); + expect(urlParamsAfterWait.get("dashboard")).toBe(dashboardParam); + testLogger.info( + "✅ URL clobber guard passed — dashboard ID and var-* value both unchanged after tab-switch drilldown" + ); + } finally { + // ── Cleanup — runs even if assertions above throw ───────────────── + await pm.dashboardList.menuItem("dashboards-item").catch(() => {}); + await deleteDashboard(page, dashboardName).catch(() => {}); + } + } + ); + } +);