312 lines
11 KiB
JavaScript
312 lines
11 KiB
JavaScript
/**
|
|
* Dashboard PromQL Query Editor Suggestions Tests
|
|
*
|
|
* FEATURE OVERVIEW:
|
|
* Tests autocomplete suggestions in the PromQL query editor:
|
|
* 1. Label name suggestions — when typing inside curly braces: metric{
|
|
* 2. Label value suggestions — when typing label_name=: metric{label_name=
|
|
* 3. Filtered label value suggestions — after typing a partial value
|
|
* 4. Selecting a label name then triggering VALUE suggestions after =
|
|
*
|
|
* FIX REFERENCE:
|
|
* - PR #9691: fix: promql query legend config suggestion issue for dashboard
|
|
*
|
|
* SELECTOR RULES (strict):
|
|
* - All locators live in the dashboard page objects under
|
|
* `pages/dashboardPages/`. The spec only calls page-object methods.
|
|
* - Only data-test selectors are used. NO `.monaco-editor`, NO
|
|
* `.suggest-widget`, NO `.monaco-list-row`, NO getByRole / getByText.
|
|
* - Suggestion assertions read Monaco's editor model + suggest controller
|
|
* via page.evaluate inside the page object. The internal Monaco DOM is
|
|
* never queried.
|
|
*/
|
|
|
|
const {
|
|
test,
|
|
expect,
|
|
navigateToBase,
|
|
} = require("../utils/enhanced-baseFixtures.js");
|
|
import PageManager from "../../pages/page-manager";
|
|
import { waitForDashboardPage, deleteDashboard } from "./utils/dashCreation.js";
|
|
const testLogger = require("../utils/test-logger.js");
|
|
const { ensureMetricsIngested } = require("../utils/shared-metrics-setup.js");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const generateUniqueDashboardName = () =>
|
|
"Dashboard_" + Math.random().toString(36).slice(2, 11) + "_" + Date.now();
|
|
|
|
/**
|
|
* Open a freshly-created dashboard, add a panel, switch the panel to PromQL
|
|
* Custom Query mode and return the page-object handle to the PromQL editor.
|
|
*
|
|
* Returns `null` if the PromQL toggle isn't available in this build — the
|
|
* caller should skip the test in that case.
|
|
*/
|
|
async function openPromqlPanel(page, pm, dashboardName, panelPrefix) {
|
|
const panelName =
|
|
pm.dashboardPanelActions.generateUniquePanelName(panelPrefix);
|
|
|
|
await pm.dashboardList.menuItem("dashboards-item");
|
|
await waitForDashboardPage(page);
|
|
await pm.dashboardCreate.waitForDashboardUIStable();
|
|
|
|
await pm.dashboardCreate.createDashboard(dashboardName);
|
|
await pm.dashboardCreate.addPanel();
|
|
await pm.dashboardPanelActions.addPanelName(panelName);
|
|
|
|
await pm.chartTypeSelector.selectChartType("line");
|
|
await pm.chartTypeSelector.selectStreamType("metrics");
|
|
|
|
const promqlAvailable =
|
|
await pm.dashboardPromQLEditor.isPromqlAvailable();
|
|
if (!promqlAvailable) {
|
|
testLogger.warn("PromQL query type button is not visible");
|
|
await pm.dashboardPromQLEditor.discardPanelIfVisible();
|
|
await deleteDashboard(page, dashboardName).catch(() => {});
|
|
return null;
|
|
}
|
|
|
|
await pm.dashboardPromQLEditor.switchToPromql();
|
|
await pm.dashboardPromQLEditor.switchToCustomQuery();
|
|
await pm.dashboardPromQLEditor.waitForEditorReady();
|
|
await pm.dashboardPromQLEditor.focusEditor();
|
|
|
|
return panelName;
|
|
}
|
|
|
|
/**
|
|
* Restore a valid PromQL query in the editor, apply, save the panel and
|
|
* delete the dashboard. Tests typically leave the editor in an invalid /
|
|
* partial state, so we overwrite via the Monaco model before applying.
|
|
*/
|
|
async function cleanupDashboard(page, pm, dashboardName) {
|
|
await pm.dashboardPromQLEditor.dismissSuggestions();
|
|
await pm.dashboardPromQLEditor.resetToValidPromqlQuery("cpu_usage{}");
|
|
|
|
// Wait for Monaco's debounced model -> Vue data sync (~500ms in CodeQueryEditor).
|
|
await page.waitForTimeout(3000);
|
|
|
|
await pm.dashboardPanelActions.applyDashboardBtn();
|
|
await pm.dashboardPanelActions.waitForChartToRender();
|
|
|
|
await pm.dashboardPanelActions.savePanel();
|
|
await pm.dashboardCreate.backToDashboardList();
|
|
await deleteDashboard(page, dashboardName);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test suite
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe.configure({ mode: "parallel" });
|
|
|
|
test.describe("Dashboard PromQL Query Editor Suggestions", () => {
|
|
// Ensure metric data is ingested once before any test runs.
|
|
test.beforeAll(async () => {
|
|
await ensureMetricsIngested();
|
|
});
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateToBase(page);
|
|
});
|
|
|
|
test("should display label name suggestions when typing inside curly braces metric{}", async ({
|
|
page,
|
|
}) => {
|
|
const dashboardName = generateUniqueDashboardName();
|
|
const pm = new PageManager(page);
|
|
const editor = pm.dashboardPromQLEditor;
|
|
|
|
const panelName = await openPromqlPanel(
|
|
page,
|
|
pm,
|
|
dashboardName,
|
|
"promql-label-suggest",
|
|
);
|
|
if (!panelName) {
|
|
test.skip(true, "PromQL button not available");
|
|
return;
|
|
}
|
|
|
|
// Type 'cpu_usage{}' and position the cursor between the braces, then
|
|
// trigger label-name suggestions for the cpu_usage metric.
|
|
await editor.clearEditor();
|
|
await editor.typeInEditor("cpu_usage{}");
|
|
await editor.pressKey("ArrowLeft");
|
|
await editor.triggerSuggestionsAndWait();
|
|
|
|
testLogger.info(
|
|
"Monaco autocomplete is open after typing cpu_usage{ inside braces",
|
|
);
|
|
|
|
const suggestionsCount = await editor.getSuggestionCount();
|
|
testLogger.info(
|
|
`Found ${suggestionsCount} label name suggestions for 'cpu_usage{}'`,
|
|
);
|
|
expect(suggestionsCount).toBeGreaterThan(0);
|
|
|
|
await editor.logSuggestions("Label name suggestions inside curly braces");
|
|
|
|
await editor.dismissSuggestions();
|
|
await cleanupDashboard(page, pm, dashboardName);
|
|
});
|
|
|
|
test("should display label VALUE suggestions when typing metric{label_name=}", async ({
|
|
page,
|
|
}) => {
|
|
const dashboardName = generateUniqueDashboardName();
|
|
const pm = new PageManager(page);
|
|
const editor = pm.dashboardPromQLEditor;
|
|
|
|
const panelName = await openPromqlPanel(
|
|
page,
|
|
pm,
|
|
dashboardName,
|
|
"promql-label-value",
|
|
);
|
|
if (!panelName) {
|
|
test.skip(true, "PromQL button not available");
|
|
return;
|
|
}
|
|
|
|
// Build cpu_usage{service_name=} with the cursor immediately after '='
|
|
// so suggestions are the VALUE candidates for service_name.
|
|
await editor.clearEditor();
|
|
await editor.typeInEditor("cpu_usage{}");
|
|
await editor.pressKey("ArrowLeft");
|
|
await editor.typeInEditor("service_name=");
|
|
await editor.triggerSuggestionsAndWait();
|
|
|
|
testLogger.info(
|
|
"Monaco autocomplete is open after typing cpu_usage{service_name=}",
|
|
);
|
|
|
|
const suggestionsCount = await editor.getSuggestionCount();
|
|
testLogger.info(
|
|
`Found ${suggestionsCount} label value suggestions for 'cpu_usage{service_name='`,
|
|
);
|
|
expect(suggestionsCount).toBeGreaterThan(0);
|
|
|
|
await editor.logSuggestions("Label value suggestions");
|
|
|
|
await editor.dismissSuggestions();
|
|
await cleanupDashboard(page, pm, dashboardName);
|
|
});
|
|
|
|
test("should display filtered label VALUE suggestions when typing partial value", async ({
|
|
page,
|
|
}) => {
|
|
const dashboardName = generateUniqueDashboardName();
|
|
const pm = new PageManager(page);
|
|
const editor = pm.dashboardPromQLEditor;
|
|
|
|
const panelName = await openPromqlPanel(
|
|
page,
|
|
pm,
|
|
dashboardName,
|
|
"promql-value-filter",
|
|
);
|
|
if (!panelName) {
|
|
test.skip(true, "PromQL button not available");
|
|
return;
|
|
}
|
|
|
|
// Build cpu_usage{service_name=} and open suggestions, then type 'a'
|
|
// to filter — verify the filtered list still surfaces at least one item.
|
|
await editor.clearEditor();
|
|
await editor.typeInEditor("cpu_usage{}");
|
|
await editor.pressKey("ArrowLeft");
|
|
await editor.typeInEditor("service_name=");
|
|
await editor.triggerSuggestionsAndWait();
|
|
|
|
await editor.typeInEditor("a");
|
|
await editor.waitForSuggestionsReady();
|
|
|
|
testLogger.info(
|
|
"Monaco autocomplete remains open after typing filter character 'a'",
|
|
);
|
|
|
|
const suggestionsCount = await editor.getSuggestionCount();
|
|
testLogger.info(
|
|
`Found ${suggestionsCount} filtered label value suggestions`,
|
|
);
|
|
expect(suggestionsCount).toBeGreaterThan(0);
|
|
|
|
await editor.logSuggestions("Filtered label value suggestions");
|
|
|
|
await editor.dismissSuggestions();
|
|
await cleanupDashboard(page, pm, dashboardName);
|
|
});
|
|
|
|
test("should select label name then show VALUE suggestions after typing =", async ({
|
|
page,
|
|
}) => {
|
|
const dashboardName = generateUniqueDashboardName();
|
|
const pm = new PageManager(page);
|
|
const editor = pm.dashboardPromQLEditor;
|
|
|
|
const panelName = await openPromqlPanel(
|
|
page,
|
|
pm,
|
|
dashboardName,
|
|
"promql-name-then-value",
|
|
);
|
|
if (!panelName) {
|
|
test.skip(true, "PromQL button not available");
|
|
return;
|
|
}
|
|
|
|
// Stage 1: trigger label NAME suggestions inside cpu_usage{}
|
|
//
|
|
// Two timing rules this test used to break, both of them about WHEN the
|
|
// suggestion list is built rather than what it contains.
|
|
//
|
|
// The list is rebuilt from the editor's `update-query`, so it belongs to
|
|
// wherever the caret was when the TEXT last changed. Typing `cpu_usage{}`
|
|
// and stepping back in with ArrowLeft never changes the text, so the list
|
|
// stays the one built past the closing brace — where metric names, not
|
|
// labels, are what belongs. Typing `cpu_usage{` and letting Monaco
|
|
// auto-close leaves the caret inside the braces on the last edit.
|
|
//
|
|
// And `update-query` is debounced by CodeQueryEditor (500ms), so pressing
|
|
// Ctrl+Space immediately after typing reads the list from BEFORE the `{`.
|
|
// That is how this test came to accept `active_sessions` — a metric name —
|
|
// as a label, and then ask for the values of a label cpu_usage does not
|
|
// have.
|
|
await editor.clearEditor();
|
|
await editor.typeInEditor("cpu_usage{");
|
|
await page.waitForTimeout(1000);
|
|
await editor.triggerSuggestionsAndWait();
|
|
|
|
const nameLabels = await editor.getSuggestionLabels(10);
|
|
expect(nameLabels.length).toBeGreaterThan(0);
|
|
// The metric's labels, not the metric list: `cpu_usage{active_sessions=`
|
|
// is not a query, and the value lookup below can only succeed for a label
|
|
// this metric actually has.
|
|
expect(nameLabels[0]).not.toContain("(");
|
|
testLogger.info(`First label name suggestion: ${nameLabels[0]}`);
|
|
|
|
// Accept the first label name suggestion.
|
|
await editor.pressKey("Enter");
|
|
|
|
// Stage 2: type '=' to switch the suggestion context to label VALUES.
|
|
// Same debounce as stage 1, plus the values themselves are a network call.
|
|
await editor.typeInEditor("=");
|
|
await page.waitForTimeout(1000);
|
|
await editor.waitForSuggestionsReady();
|
|
|
|
const valueLabels = await editor.getSuggestionLabels(5);
|
|
testLogger.info(
|
|
`Found ${valueLabels.length} label value suggestions after typing =`,
|
|
);
|
|
testLogger.info(`Label value suggestions: ${valueLabels.join(", ")}`);
|
|
expect(valueLabels.length).toBeGreaterThan(0);
|
|
|
|
await editor.dismissSuggestions();
|
|
await cleanupDashboard(page, pm, dashboardName);
|
|
});
|
|
});
|