diff --git a/tests/ui-testing/ci-matrix/ci_matrix.json b/tests/ui-testing/ci-matrix/ci_matrix.json
index d844a1d91f..4e2e89a2fb 100644
--- a/tests/ui-testing/ci-matrix/ci_matrix.json
+++ b/tests/ui-testing/ci-matrix/ci_matrix.json
@@ -29,7 +29,8 @@
"alertDestinationsPagination.spec.js",
"alerts-destinations-usage.spec.js",
"alerts-save-query-mode.spec.js",
- "alerts-no-destination.spec.js"
+ "alerts-no-destination.spec.js",
+ "alerts-from-panel.spec.js"
],
"disabled": [
{
@@ -561,7 +562,7 @@
},
{
"testfolder": "Synthetics",
- "_comment": "Own shard because the synthetics routes only exist when the server runs with ZO_SYNTHETICS_ENABLED=true (router registers /api/{org}/synthetics* conditionally). The flag is read once at process start, so it cannot vary between tests sharing a backend — each shard gets its own OpenObserve process; every other shard omits synthetics_enabled and keeps the default (false). Pinned to 3 workers: every create/list test writes a check and 5 concurrent writers trip SQLite's 'database is locked' (SLO shard precedent). Results/run-detail pages are fed by seeding the synthetics_results streams in beforeAll — no probe runs on CI. status-pages-enterprise-gating lives here because the status-page routes sit inside the same gate.",
+ "_comment": "Own shard because the synthetics routes only exist when the server runs with ZO_SYNTHETICS_ENABLED=true (router registers /api/{org}/synthetics* conditionally). The flag is read once at process start, so it cannot vary between tests sharing a backend \u2014 each shard gets its own OpenObserve process; every other shard omits synthetics_enabled and keeps the default (false). Pinned to 3 workers: every create/list test writes a check and 5 concurrent writers trip SQLite's 'database is locked' (SLO shard precedent). Results/run-detail pages are fed by seeding the synthetics_results streams in beforeAll \u2014 no probe runs on CI. status-pages-enterprise-gating lives here because the status-page routes sit inside the same gate.",
"actual_folder": "Synthetics",
"browser": "chrome",
"synthetics_enabled": "true",
diff --git a/tests/ui-testing/pages/alertsPages/alertsPage.js b/tests/ui-testing/pages/alertsPages/alertsPage.js
index 5b817f812e..628864a986 100644
--- a/tests/ui-testing/pages/alertsPages/alertsPage.js
+++ b/tests/ui-testing/pages/alertsPages/alertsPage.js
@@ -3255,6 +3255,11 @@ export class AlertsPage {
return ((await valueEl.textContent()) ?? '').trim();
}
+ /** Display-mode alert name value element (OFormInlineEdit span, not an ). */
+ getAlertNameValueLocator() {
+ return this.page.locator('[data-test="add-alert-name-input-value"]').first();
+ }
+
/**
* Select stream type from dropdown
* OSelect popover pattern: open trigger, wait for popover, click option keyed by data-test-value (§4)
diff --git a/tests/ui-testing/pages/alertsPages/createAlertFromSourceDialogPage.js b/tests/ui-testing/pages/alertsPages/createAlertFromSourceDialogPage.js
new file mode 100644
index 0000000000..70b51dde67
--- /dev/null
+++ b/tests/ui-testing/pages/alertsPages/createAlertFromSourceDialogPage.js
@@ -0,0 +1,94 @@
+// Create Alert From Source confirm-dialog page object.
+// The shared "create an alert from here" confirmation step (app-root mounted via
+// CreateAlertDialogProvider in App.vue). Every section renders off the prefill,
+// so this owns the dialog shell + the conditional sections the specs assert on.
+import { expect } from "@playwright/test";
+
+export default class CreateAlertFromSourceDialogPage {
+ constructor(page) {
+ this.page = page;
+
+ // Dialog shell + gating visibility (continue/cancel are ODialog buttons inside it).
+ this.dialog = page.locator('[data-test="create-alert-from-source-dialog"]');
+ this.continueBtn = page.locator(
+ '[data-test="create-alert-from-source-dialog"] [data-test="o-dialog-primary-btn"]'
+ );
+ this.cancelBtn = page.locator(
+ '[data-test="create-alert-from-source-dialog"] [data-test="o-dialog-secondary-btn"]'
+ );
+
+ // Query-picker mode (pickQueryOnly): "pick which query" + choice OSelect + picked text.
+ this.queryPickerIntro = page.locator('[data-test="create-alert-query-picker-intro"]');
+ this.queryPicker = page.locator('[data-test="create-alert-query-picker"]');
+ this.queryPickerTrigger = page.locator('[data-test="create-alert-query-picker-trigger"]');
+ this.queryPickerOption = page.locator('[data-test="create-alert-query-picker-option"]');
+ this.pickedQuery = page.locator('[data-test="create-alert-picked-query"]');
+ this.pickedQueryText = page.locator('[data-test="create-alert-picked-query-text"]');
+
+ // Sections suppressed while pickQueryOnly is active (multi-query panel).
+ this.thresholdShape = page.locator('[data-test="create-alert-threshold-shape"]');
+ this.window = page.locator('[data-test="create-alert-window"]');
+ this.queryPreview = page.locator('[data-test="create-alert-query-preview"]');
+ }
+
+ async expectDialogVisible() {
+ await expect(this.dialog).toBeVisible({ timeout: 15000 });
+ }
+
+ async expectDialogHidden() {
+ await expect(this.dialog).toBeHidden({ timeout: 15000 });
+ }
+
+ async expectQueryPickerVisible() {
+ await expect(this.queryPicker).toBeVisible({ timeout: 10000 });
+ }
+
+ async expectPickedQueryTextVisible() {
+ await expect(this.pickedQueryText).toBeVisible({ timeout: 10000 });
+ }
+
+ /** The picked-query text (colorized SQL), trimmed; empty while unset. */
+ async getPickedQueryText() {
+ await this.pickedQueryText.waitFor({ state: "visible", timeout: 10000 });
+ return (await this.pickedQueryText.textContent())?.trim() ?? "";
+ }
+
+ async expectThresholdShapeHidden() {
+ await expect(this.thresholdShape).toHaveCount(0);
+ }
+
+ async expectWindowHidden() {
+ await expect(this.window).toHaveCount(0);
+ }
+
+ async expectQueryPreviewHidden() {
+ await expect(this.queryPreview).toHaveCount(0);
+ }
+
+ /** Open the query picker and choose the query at the given (0-based) index. */
+ async selectQuery(index) {
+ await this.queryPickerTrigger.click();
+ await this.queryPickerOption
+ .nth(index)
+ .waitFor({ state: "visible", timeout: 10000 });
+ await this.queryPickerOption.nth(index).click();
+ }
+
+ async clickContinue() {
+ await this.continueBtn.click();
+ }
+
+ async clickCancel() {
+ await this.cancelBtn.click();
+ }
+
+ async expectContinueDisabled() {
+ await expect(this.continueBtn).toBeDisabled();
+ }
+
+ async expectWarningVisible(key) {
+ await expect(
+ this.page.locator(`[data-test="create-alert-warning-${key}"]`)
+ ).toBeVisible({ timeout: 10000 });
+ }
+}
diff --git a/tests/ui-testing/pages/dashboardPages/dashboard-panel-edit.js b/tests/ui-testing/pages/dashboardPages/dashboard-panel-edit.js
index a7ac2f8c4f..ec55d25b35 100644
--- a/tests/ui-testing/pages/dashboardPages/dashboard-panel-edit.js
+++ b/tests/ui-testing/pages/dashboardPages/dashboard-panel-edit.js
@@ -325,6 +325,18 @@ export default class DashboardPanel {
return this.page.locator('[data-test="dashboard-panel-container"]');
}
+ // Create Alert menu item (dropdown item) for disabled/state assertions.
+ getCreateAlertFromPanelItem() {
+ return this.createAlertFromPanel;
+ }
+
+ // The disabled-reason description rendered under the item (ODropdownItem).
+ getCreateAlertFromPanelReason() {
+ return this.page.locator(
+ '[data-test="dashboard-create-alert-from-panel-reason"]'
+ );
+ }
+
// Alert context menu options (shown on chart right-click).
getAlertContextMenuAbove() {
return this.alertContextMenuAbove;
diff --git a/tests/ui-testing/pages/page-manager.js b/tests/ui-testing/pages/page-manager.js
index e612aa04ce..fae284c414 100644
--- a/tests/ui-testing/pages/page-manager.js
+++ b/tests/ui-testing/pages/page-manager.js
@@ -31,6 +31,7 @@ import LogsVisualise from "./dashboardPages/visualise";
import { DashboardPage } from "./dashboardPages/dashboardPage.js";
import { ScheduledReportsDrawerPage } from "./dashboardPages/scheduledReportsDrawer.js";
import { AlertsPage } from "./alertsPages/alertsPage.js";
+import CreateAlertFromSourceDialogPage from "./alertsPages/createAlertFromSourceDialogPage.js";
import { AlertLibraryPage } from "./alertsPages/alertLibraryPage.js";
import { AlertHistoryPage } from "./alertsPages/alertHistoryPage.js";
import { AlertDetailPage } from "./alertsPages/alertDetailPage.js";
@@ -201,6 +202,7 @@ class PageManager {
// ===== EXISTING ALERTS PAGE OBJECT =====
this.alertsPage = new AlertsPage(page);
+ this.createAlertFromSourceDialog = new CreateAlertFromSourceDialogPage(page);
this.alertLibraryPage = new AlertLibraryPage(page);
this.alertHistoryPage = new AlertHistoryPage(page);
this.alertDetailPage = new AlertDetailPage(page);
diff --git a/tests/ui-testing/playwright-tests/Alerts/alerts-from-panel.spec.js b/tests/ui-testing/playwright-tests/Alerts/alerts-from-panel.spec.js
new file mode 100644
index 0000000000..c57b67da8a
--- /dev/null
+++ b/tests/ui-testing/playwright-tests/Alerts/alerts-from-panel.spec.js
@@ -0,0 +1,363 @@
+// Create Alert from Dashboard Panel - E2E Tests
+// Covers the shared "create an alert from here" confirm dialog when entered from
+// a dashboard panel (source: "panel"). The headline path is a multi-query panel
+// opening the dialog in pickQueryOnly mode (query picker + picked query, with
+// threshold/window/preview suppressed), plus the disabled menu item and cancel.
+
+import {
+ test,
+ expect,
+ navigateToBase,
+} from "../utils/enhanced-baseFixtures.js";
+import PageManager from "../../pages/page-manager";
+import { ingestion } from "../Dashboards/utils/dashIngestion.js";
+import {
+ waitForDashboardPage,
+ deleteDashboard,
+} from "../Dashboards/utils/dashCreation.js";
+import { waitForStreamComplete } from "../utils/streaming-helpers.js";
+import testLogger from "../utils/test-logger.js";
+
+test.describe("Create Alert from Dashboard Panel testcases", () => {
+ test.describe.configure({ mode: "parallel" });
+ let pm;
+
+ test.beforeEach(async ({ page }, testInfo) => {
+ testLogger.testStart(testInfo.title, testInfo.file);
+ await navigateToBase(page);
+ pm = new PageManager(page);
+ await ingestion(page);
+
+ // navigateToBase() alone can land on the wrong org's page on cloud; force
+ // the correct org with an explicit navigation to a real feature page.
+ await page.goto(
+ `${process.env["ZO_BASE_URL"]}/web/dashboards?org_identifier=${process.env["ORGNAME"]}&folder=default`
+ );
+ await page.waitForLoadState("domcontentloaded");
+ testLogger.info("Test setup completed");
+ });
+
+ test(
+ "should open the query-picker confirm dialog from a multi-query panel and land on the pre-filled alert form",
+ { tag: ["@create-alert-from-panel", "@alerts", "@all", "@P0"] },
+ async ({ page }) => {
+ testLogger.info("Testing multi-query panel -> confirm dialog -> alert form");
+
+ const dashName =
+ "Dashboard_Alert_MQ_" + Math.random().toString(36).slice(2, 11);
+ const panelName =
+ pm.dashboardPanelActions.generateUniquePanelName("alert-mq");
+
+ await pm.dashboardList.menuItem("dashboards-item");
+ await waitForDashboardPage(page);
+
+ await pm.dashboardCreate.createDashboard(dashName);
+
+ // Bar chart panel with two visible queries, each on e2e_automate.
+ await pm.dashboardCreate.addPanel();
+ await pm.dashboardPanelActions.addPanelName(panelName);
+
+ // Q1
+ await pm.chartTypeSelector.selectChartType("bar");
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_container_name",
+ "y"
+ );
+
+ // Q2 — a new query tab becomes active; give it its own stream + measure.
+ await pm.dashboardMultiSQL.addQueryTab(1);
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_namespace_name",
+ "y"
+ );
+
+ const streamPromise = waitForStreamComplete(page);
+ await pm.dashboardPanelActions.applyDashboardBtn();
+ await streamPromise;
+ await pm.dashboardPanelActions.waitForChartToRender();
+
+ const dashboardStreamPromise = waitForStreamComplete(page, 30000);
+ await pm.dashboardPanelActions.savePanel();
+ await dashboardStreamPromise;
+ await pm.dashboardPanelActions
+ .getChartRendererCanvasElement()
+ .first()
+ .waitFor({ state: "visible", timeout: 15000 });
+
+ await pm.dashboardPanelEdit.getPanelContainer().first().hover();
+ await pm.dashboardPanelEdit.createAlertFromPanelMenu(panelName);
+
+ // The confirm dialog appears only because there are >= 2 queries.
+ await pm.createAlertFromSourceDialog.expectDialogVisible();
+ await pm.createAlertFromSourceDialog.expectQueryPickerVisible();
+
+ // pickQueryOnly shows the picked query; threshold/window/preview are suppressed.
+ await pm.createAlertFromSourceDialog.expectPickedQueryTextVisible();
+ expect(
+ await pm.createAlertFromSourceDialog.getPickedQueryText()
+ ).not.toBe("");
+ await pm.createAlertFromSourceDialog.expectThresholdShapeHidden();
+ await pm.createAlertFromSourceDialog.expectWindowHidden();
+ await pm.createAlertFromSourceDialog.expectQueryPreviewHidden();
+
+ await pm.createAlertFromSourceDialog.clickContinue();
+
+ await page.waitForURL(/.*alerts\/add.*prefill=panel.*/, {
+ timeout: 30000,
+ });
+ const currentUrl = page.url();
+ expect(currentUrl).toContain("prefill=panel");
+ // The panel payload rides sessionStorage, not the URL.
+ expect(currentUrl).not.toContain("panelData=");
+
+ // The alert name is an OFormInlineEdit title: in display mode the value
+ // is a span, not an , so read the committed value via the helper.
+ const alertNameValue = pm.alertsPage.getAlertNameValueLocator();
+ await expect(alertNameValue).toBeVisible({ timeout: 30000 });
+ expect((await alertNameValue.textContent()) ?? "").toContain("Alert_from_");
+ testLogger.info("Landed on the alert form pre-filled from the panel");
+
+ // Navigate back to the dashboard folder and clean up.
+ await page.goto(
+ `${process.env["ZO_BASE_URL"]}/web/dashboards?org_identifier=${process.env["ORGNAME"]}&folder=default`
+ );
+ await page.waitForLoadState("domcontentloaded");
+ await waitForDashboardPage(page);
+ await pm.dashboardFolder
+ .getFolderCardByName("default")
+ .waitFor({ state: "visible", timeout: 30000 });
+ await pm.dashboardFolder.openFolderByName("default");
+ await deleteDashboard(page, dashName);
+
+ testLogger.info("Test completed: multi-query panel confirm dialog");
+ }
+ );
+
+ test(
+ "should update the picked-query text when a different query is selected in the dialog",
+ { tag: ["@create-alert-from-panel", "@alerts", "@all", "@P1"] },
+ async ({ page }) => {
+ testLogger.info("Testing query picker re-parameterisation (rebuild)");
+
+ const dashName =
+ "Dashboard_Alert_RB_" + Math.random().toString(36).slice(2, 11);
+ const panelName =
+ pm.dashboardPanelActions.generateUniquePanelName("alert-rebuild");
+
+ await pm.dashboardList.menuItem("dashboards-item");
+ await waitForDashboardPage(page);
+
+ await pm.dashboardCreate.createDashboard(dashName);
+
+ await pm.dashboardCreate.addPanel();
+ await pm.dashboardPanelActions.addPanelName(panelName);
+
+ await pm.chartTypeSelector.selectChartType("bar");
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_container_name",
+ "y"
+ );
+
+ await pm.dashboardMultiSQL.addQueryTab(1);
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_namespace_name",
+ "y"
+ );
+
+ const streamPromise = waitForStreamComplete(page);
+ await pm.dashboardPanelActions.applyDashboardBtn();
+ await streamPromise;
+ await pm.dashboardPanelActions.waitForChartToRender();
+
+ const dashboardStreamPromise = waitForStreamComplete(page, 30000);
+ await pm.dashboardPanelActions.savePanel();
+ await dashboardStreamPromise;
+ await pm.dashboardPanelActions
+ .getChartRendererCanvasElement()
+ .first()
+ .waitFor({ state: "visible", timeout: 15000 });
+
+ await pm.dashboardPanelEdit.getPanelContainer().first().hover();
+ await pm.dashboardPanelEdit.createAlertFromPanelMenu(panelName);
+
+ await pm.createAlertFromSourceDialog.expectDialogVisible();
+ const initialText =
+ await pm.createAlertFromSourceDialog.getPickedQueryText();
+ expect(initialText).not.toBe("");
+
+ await pm.createAlertFromSourceDialog.selectQuery(1);
+
+ // The picked-query display follows the newly chosen query.
+ await expect
+ .poll(
+ async () =>
+ await pm.createAlertFromSourceDialog.getPickedQueryText(),
+ { timeout: 15000 }
+ )
+ .not.toBe(initialText);
+
+ await pm.createAlertFromSourceDialog.clickCancel();
+ await pm.createAlertFromSourceDialog.expectDialogHidden();
+
+ await pm.dashboardCreate.backToDashboardList();
+ await deleteDashboard(page, dashName);
+
+ testLogger.info("Test completed: query picker rebuild");
+ }
+ );
+
+ test(
+ "should close the confirm dialog on Cancel and stay on the dashboard",
+ { tag: ["@create-alert-from-panel", "@alerts", "@all", "@P1"] },
+ async ({ page }) => {
+ testLogger.info("Testing Cancel on the confirm dialog");
+
+ const dashName =
+ "Dashboard_Alert_Cancel_" + Math.random().toString(36).slice(2, 11);
+ const panelName =
+ pm.dashboardPanelActions.generateUniquePanelName("alert-cancel");
+
+ await pm.dashboardList.menuItem("dashboards-item");
+ await waitForDashboardPage(page);
+
+ await pm.dashboardCreate.createDashboard(dashName);
+
+ await pm.dashboardCreate.addPanel();
+ await pm.dashboardPanelActions.addPanelName(panelName);
+
+ await pm.chartTypeSelector.selectChartType("bar");
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_container_name",
+ "y"
+ );
+
+ await pm.dashboardMultiSQL.addQueryTab(1);
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_namespace_name",
+ "y"
+ );
+
+ const streamPromise = waitForStreamComplete(page);
+ await pm.dashboardPanelActions.applyDashboardBtn();
+ await streamPromise;
+ await pm.dashboardPanelActions.waitForChartToRender();
+
+ const dashboardStreamPromise = waitForStreamComplete(page, 30000);
+ await pm.dashboardPanelActions.savePanel();
+ await dashboardStreamPromise;
+ await pm.dashboardPanelActions
+ .getChartRendererCanvasElement()
+ .first()
+ .waitFor({ state: "visible", timeout: 15000 });
+
+ await pm.dashboardPanelEdit.getPanelContainer().first().hover();
+ await pm.dashboardPanelEdit.createAlertFromPanelMenu(panelName);
+
+ await pm.createAlertFromSourceDialog.expectDialogVisible();
+ await pm.createAlertFromSourceDialog.clickCancel();
+ await pm.createAlertFromSourceDialog.expectDialogHidden();
+
+ // No route change: still on the dashboard, not the alert form.
+ expect(page.url()).not.toContain("alerts/add");
+
+ await pm.dashboardCreate.backToDashboardList();
+ await deleteDashboard(page, dashName);
+
+ testLogger.info("Test completed: Cancel closes the confirm dialog");
+ }
+ );
+
+ test(
+ "should disable the Create Alert menu item with a reason when the panel has no visible query",
+ { tag: ["@create-alert-from-panel", "@alerts", "@all", "@P1"] },
+ async ({ page }) => {
+ testLogger.info("Testing disabled Create Alert menu item (no visible query)");
+
+ const dashName =
+ "Dashboard_Alert_Dsb_" + Math.random().toString(36).slice(2, 11);
+ const panelName =
+ pm.dashboardPanelActions.generateUniquePanelName("alert-disabled");
+
+ await pm.dashboardList.menuItem("dashboards-item");
+ await waitForDashboardPage(page);
+
+ await pm.dashboardCreate.createDashboard(dashName);
+
+ await pm.dashboardCreate.addPanel();
+ await pm.dashboardPanelActions.addPanelName(panelName);
+
+ await pm.chartTypeSelector.selectChartType("bar");
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_container_name",
+ "y"
+ );
+
+ // The visibility toggle only appears from the second query onwards, so
+ // add a second query and hide BOTH to leave the panel with no visible query.
+ await pm.dashboardMultiSQL.addQueryTab(1);
+ await pm.chartTypeSelector.selectStreamType("logs");
+ await pm.chartTypeSelector.selectStream("e2e_automate");
+ await pm.chartTypeSelector.searchAndAddField(
+ "kubernetes_namespace_name",
+ "y"
+ );
+
+ const streamPromise = waitForStreamComplete(page);
+ await pm.dashboardPanelActions.applyDashboardBtn();
+ await streamPromise;
+ await pm.dashboardPanelActions.waitForChartToRender();
+
+ // Hide both queries so the saved panel has zero visible queries.
+ await pm.dashboardMultiSQL.toggleQueryVisibility(0);
+ await pm.dashboardMultiSQL.toggleQueryVisibility(1);
+
+ const dashboardStreamPromise = waitForStreamComplete(page, 30000);
+ await pm.dashboardPanelActions.savePanel();
+ await dashboardStreamPromise;
+
+ await pm.dashboardPanelEdit.getPanelContainer().first().hover();
+ await pm.dashboardPanelActions.getEditPanelDropdown(panelName).click();
+
+ const createAlertItem = pm.dashboardPanelEdit.getCreateAlertFromPanelItem();
+ await expect(createAlertItem).toBeVisible({ timeout: 10000 });
+ await expect(createAlertItem).toBeDisabled();
+ await expect(
+ pm.dashboardPanelEdit.getCreateAlertFromPanelReason()
+ ).toHaveText("This panel has no queries to create an alert from");
+
+ await pm.dashboardCreate.backToDashboardList();
+ await deleteDashboard(page, dashName);
+
+ testLogger.info("Test completed: disabled Create Alert menu item");
+ }
+ );
+
+ test.fixme(
+ "should block Continue in the confirm dialog when the prefill carries an unresolved-query warning — deferred: unresolved-variable panel setup unproven in CI (fromPanel.ts:293-295)",
+ { tag: ["@create-alert-from-panel", "@alerts", "@all", "@P2"] },
+ async ({ page }) => {
+ // A panel query referencing an unresolved dashboard variable ($var) with
+ // no executedQuery produces a blocking `unresolvedQuery` warning, which
+ // disables Continue. Constructing that panel deterministically (a $var
+ // that does not resolve) has no proven helper in the suite yet.
+ testLogger.info("Testing blocking warning disables Continue (deferred)");
+
+ await pm.createAlertFromSourceDialog.expectWarningVisible("unresolvedQuery");
+ await pm.createAlertFromSourceDialog.expectContinueDisabled();
+ }
+ );
+});