mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-03 16:10:21 +00:00
test: enhance logs visualization error handling and persistence tests (#13591)
## Summary Adds **10 E2E tests** covering the two defects fixed in #13244, plus the `SELECT *` behaviour change that shipped alongside them. Also introduces per-shard backend configuration so the `SELECT *` tests can actually run in CI. ## Test coverage ### `logsVisualizePersistence.spec.js` — 7 tests (quick mode **off**) | # | Test | Defect | Fix code exercised | |---|---|---|---| | 1 | Renders without the false required-fields error | A | `convertPanelData.ts` `!query?.customQuery` | | 2 | Restores chart type, query and chart after URL refresh (h-bar) | B | `setupLogsTab` re-run + `isInitialVisualizeRestore` | | 3 | Re-renders after reload with the default chart type | B | permanent No-Data half | | 4 | Restores a non-default chart type (area) after refresh | B | `restoreVisualizationFromUrlOnLoad()` | | 5 | Chart still renders after reload with a VRL function applied | B | `prepareVisualizeMode()` VRL copy | | 6 | Never shows required-fields error toggling Visualize → Logs → Visualize | A | manual-toggle rebuild path | | 7 | No `SELECT *` toast opening Visualize with no stream in the URL | B | page-load ordering guard | **Defect A** — false "Please select required fields to render the chart" on the custom-query Timechart panel. **Defect B** — URL refresh produced a `SELECT *` toast plus permanent No Data. ### `logsVisualizeSelectStar.spec.js` — 3 tests (quick mode **on**, dedicated shard) | # | Test | Behaviour verified | Call site | |---|---|---|---| | 8 | Table chart on `SELECT *` → toast shown **and** chart type reverts | Table is blocked | `Index.vue:2265` (watcher) | | 9 | Histogram chart on `SELECT *` → no toast, renders | Line/bar/area/scatter allowed | entry path | | 10 | `SELECT *` line chart survives browser refresh → no toast, state restored | Restore path doesn't re-emit | `Index.vue:2465` (restore) | ## Infrastructure changes | File | Change | Why | |---|---|---| | `ci_matrix.json` | New `Logs-SelectStar` shard with `quick_mode_enabled: "true"` | `isSelectStarForTable()` only exists when the server runs with quick mode on | | `playwright.yml` | Per-shard `ZO_QUICK_MODE_ENABLED` env on `Start OpenObserve` | The flag is read once at server start — a shard is the smallest unit that can vary it | | `build-ci-matrix.js` | Emit `quick_mode_enabled` (defaults `"false"`) | The field was being silently stripped by the emit whitelist | | `logsPage.js` | 13 assertion helpers + fixed chart-type detection | Detection read `data-selected` off the wrong element and matched retired CSS classes, so it always returned `null` | All 24 existing shards are unaffected — they omit the field and default to `false`. ## Notes on test quality - Every negative assertion ("no toast", "no error") is sequenced after a deterministic positive end-state, and gated on a precondition so it cannot pass vacuously. - Toast absence is proved with a MutationObserver recorder installed before the navigation under test, since toasts auto-dismiss. - The quick-mode precondition is a hard assertion, not `test.skip` — a misconfigured shard fails loudly instead of silently reporting "skipped".
This commit is contained in:
@@ -104,11 +104,18 @@ for (const s of include) {
|
||||
|
||||
// Emit ONLY the fields the CI matrix consumes — disabled/_comment/notes are stripped,
|
||||
// so turned-off specs never reach `npx playwright test`.
|
||||
//
|
||||
// quick_mode_enabled is always emitted (defaulting to "false") so every shard
|
||||
// carries the key: the workflow feeds it to ZO_QUICK_MODE_ENABLED when starting
|
||||
// that shard's OpenObserve process. The flag is read once at server start, so it
|
||||
// cannot vary per test — a shard is the smallest unit that can change it.
|
||||
const matrix = include.map((s) => ({
|
||||
testfolder: s.testfolder,
|
||||
actual_folder: s.actual_folder,
|
||||
browser: s.browser || "chrome",
|
||||
run_files: s.run_files,
|
||||
quick_mode_enabled:
|
||||
s.quick_mode_enabled === true || s.quick_mode_enabled === "true" ? "true" : "false",
|
||||
}));
|
||||
|
||||
process.stdout.write(JSON.stringify({ include: matrix }));
|
||||
|
||||
@@ -247,6 +247,12 @@ jobs:
|
||||
path: /tmp/dist
|
||||
|
||||
- name: Start OpenObserve
|
||||
# Per-shard override of the workflow-level default. ZO_QUICK_MODE_ENABLED is
|
||||
# read once at server start and is global to the process, so a shard is the
|
||||
# smallest unit that can change it. Set via "quick_mode_enabled" in
|
||||
# tests/ui-testing/ci-matrix/ci_matrix.json; shards that omit it get "false".
|
||||
env:
|
||||
ZO_QUICK_MODE_ENABLED: ${{ matrix.quick_mode_enabled || 'false' }}
|
||||
run: chmod +x ./release-ci-binary/openobserve && ./release-ci-binary/openobserve > o2.log 2>&1 &
|
||||
|
||||
- name: Wait for start
|
||||
|
||||
@@ -75,6 +75,17 @@
|
||||
],
|
||||
"disabled": []
|
||||
},
|
||||
{
|
||||
"_comment": "Own shard because isSelectStarForTable() only exists when the server runs with ZO_QUICK_MODE_ENABLED=true. That 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 quick_mode_enabled and keeps the default (false).",
|
||||
"testfolder": "Logs-SelectStar",
|
||||
"actual_folder": "Logs",
|
||||
"browser": "chrome",
|
||||
"quick_mode_enabled": "true",
|
||||
"run_files": [
|
||||
"logsVisualizeSelectStar.spec.js"
|
||||
],
|
||||
"disabled": []
|
||||
},
|
||||
{
|
||||
"testfolder": "Logs-Features",
|
||||
"actual_folder": "Logs",
|
||||
|
||||
@@ -305,6 +305,20 @@ export class LogsPage {
|
||||
this.dashboardPanelTable = '[data-test="dashboard-panel-table"]';
|
||||
// Composite: any of the three indicates the build/visualize tab finished initial render.
|
||||
this.buildInitIndicator = `${this.chartRenderer}, ${this.dashboardPanelTable}, ${this.noDataMessage}`;
|
||||
// Composite: a *painted* chart — either an echarts canvas or a rendered table panel.
|
||||
// Scoped with :visible because the logs histogram keeps its own (hidden) chart-renderer
|
||||
// in the DOM while the Visualize tab is active.
|
||||
this.renderedChartIndicator = `${this.chartRenderer} canvas:visible, ${this.dashboardPanelTable}:visible`;
|
||||
|
||||
// ===== VISUALIZE ERROR PANEL (DashboardErrors.vue) =====
|
||||
// Rendered only when errorData.errors is non-empty (v-if), so count 0 == no errors.
|
||||
this.dashboardError = '[data-test="dashboard-error"]';
|
||||
this.dashboardErrorsListItem = '[data-test="dashboard-errors-list-item"]';
|
||||
// Error text thrown by convertPanelData() for builder-mode panels with empty x/y
|
||||
// (web/src/utils/dashboard/convertPanelData.ts).
|
||||
this.requiredFieldsErrorText = 'Please select required fields to render the chart';
|
||||
// logs.index.selectStarNotSupportedForVisualization (en-US.json)
|
||||
this.selectStarNotSupportedToastText = 'Select * query is not supported for visualization';
|
||||
|
||||
// ===== SHARE LINK SELECTORS (VERIFIED) =====
|
||||
this.shareLinkButton = '[data-test="logs-search-bar-share-link-btn"]';
|
||||
@@ -9435,6 +9449,250 @@ export class LogsPage {
|
||||
testLogger.info('Live mode refresh-interval button is visible');
|
||||
}
|
||||
|
||||
// ===== LOGS VISUALIZE — CHART RENDER / ERROR ASSERTIONS (PR #13244, issue #12897) =====
|
||||
|
||||
/**
|
||||
* Wait for the Visualize (Timechart) panel to actually PAINT a chart, then
|
||||
* assert the "No Data" empty state is gone.
|
||||
*
|
||||
* This is the deterministic positive end-state that every negative assertion
|
||||
* below must be sequenced after — never assert "no error" against a panel
|
||||
* that has not finished rendering yet.
|
||||
* @param {number} timeout - Max wait for the chart to paint (default 45000)
|
||||
*/
|
||||
async expectVisualizeChartRendered(timeout = 45000) {
|
||||
await expect(this.page.locator(this.renderedChartIndicator).first())
|
||||
.toBeVisible({ timeout });
|
||||
// `no-data` is a v-if empty state, so it is removed from the DOM once the
|
||||
// panel has rows. toHaveCount(0) auto-retries, so a brief no-data flash
|
||||
// while the first chunk streams in does not fail the assertion.
|
||||
await expect(this.page.locator(this.noDataMessage)).toHaveCount(0, { timeout: 15000 });
|
||||
testLogger.info('Visualize chart is rendered (not the No Data state)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the Visualize errors panel (DashboardErrors.vue) is absent.
|
||||
* The container is rendered behind `v-if="errors.length"`, and errors are only
|
||||
* cleared by an explicit resetErrors() — i.e. an error raised at any point in
|
||||
* the current run is still present when this runs. Call AFTER
|
||||
* expectVisualizeChartRendered() so the panel has settled.
|
||||
*/
|
||||
async expectNoDashboardErrors() {
|
||||
await expect(this.page.locator(this.dashboardError)).toHaveCount(0, { timeout: 15000 });
|
||||
testLogger.info('No dashboard error panel is present');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the false "Please select required fields to render the chart" error
|
||||
* (convertPanelData's builder-mode guard) never landed in the errors panel.
|
||||
* Targets the message specifically so an unrelated backend error still reports
|
||||
* a meaningful failure via expectNoDashboardErrors().
|
||||
*/
|
||||
async expectNoRequiredFieldsError() {
|
||||
const errorItems = this.page.locator(this.dashboardErrorsListItem, {
|
||||
hasText: this.requiredFieldsErrorText,
|
||||
});
|
||||
await expect(errorItems).toHaveCount(0, { timeout: 15000 });
|
||||
testLogger.info('No "required fields" error is present in the errors panel');
|
||||
}
|
||||
|
||||
// ===== LOGS VISUALIZE — TOAST RECORDER =====
|
||||
|
||||
/**
|
||||
* Install a toast recorder that survives navigation.
|
||||
*
|
||||
* Toasts auto-dismiss, so polling for one after the page has settled is
|
||||
* inherently racy — the toast we are asserting the ABSENCE of may have come
|
||||
* and gone. Instead this attaches a MutationObserver that records every
|
||||
* `[data-test="o-toast-message"]` text into `window.__o2RecordedToasts`, and
|
||||
* registers it as an init script so it is re-installed at document-start on
|
||||
* every subsequent navigation (including `page.reload()`). The assertion then
|
||||
* inspects the recorded log after a deterministic positive end-state.
|
||||
*
|
||||
* Call this BEFORE the navigation/reload under test.
|
||||
*/
|
||||
async startToastRecorder() {
|
||||
const installer = () => {
|
||||
if (window.__o2ToastRecorderInstalled) return;
|
||||
window.__o2ToastRecorderInstalled = true;
|
||||
window.__o2RecordedToasts = [];
|
||||
const SELECTOR = '[data-test="o-toast-message"]';
|
||||
const scan = () => {
|
||||
document.querySelectorAll(SELECTOR).forEach((el) => {
|
||||
const text = (el.textContent || '').trim();
|
||||
if (text && !window.__o2RecordedToasts.includes(text)) {
|
||||
window.__o2RecordedToasts.push(text);
|
||||
}
|
||||
});
|
||||
};
|
||||
const start = () => {
|
||||
scan();
|
||||
// characterData is required too: Vue inserts the toast node first and
|
||||
// fills its text on a later tick, so childList alone can miss the text.
|
||||
new MutationObserver(scan).observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
};
|
||||
if (document.documentElement) {
|
||||
start();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', start);
|
||||
}
|
||||
};
|
||||
await this.page.addInitScript(installer);
|
||||
// Also install on the CURRENT document — addInitScript only affects future loads.
|
||||
await this.page.evaluate(installer).catch(() => {});
|
||||
testLogger.info('Toast recorder installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read every toast message recorded since startToastRecorder().
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async getRecordedToastMessages() {
|
||||
return await this.page.evaluate(() => window.__o2RecordedToasts ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert no recorded toast contains the given text.
|
||||
* @param {string} text - Substring that must not appear in any recorded toast
|
||||
*/
|
||||
async expectNoToastContaining(text) {
|
||||
const messages = await this.getRecordedToastMessages();
|
||||
const matches = messages.filter((m) => m.includes(text));
|
||||
expect(
|
||||
matches,
|
||||
`Expected no toast containing "${text}". Recorded toasts: ${JSON.stringify(messages)}`
|
||||
).toEqual([]);
|
||||
testLogger.info(`No toast containing "${text}" was emitted`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the "Select * query is not supported for visualization" toast was
|
||||
* never emitted (the symptom of the `select * from "undefined"` page-load race).
|
||||
*/
|
||||
async expectNoSelectStarVisualizationToast() {
|
||||
await this.expectNoToastContaining(this.selectStarNotSupportedToastText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a toast containing the given text WAS emitted since startToastRecorder().
|
||||
* Polls because the toast arrives asynchronously after the triggering action.
|
||||
* @param {string} text - Substring expected in one of the recorded toasts.
|
||||
*/
|
||||
async expectToastContaining(text, timeout = 15000) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await this.getRecordedToastMessages()).some((m) => m.includes(text)),
|
||||
{
|
||||
timeout,
|
||||
message: `Expected a toast containing "${text}" but none was emitted`,
|
||||
}
|
||||
)
|
||||
.toBe(true);
|
||||
testLogger.info(`Toast containing "${text}" was emitted`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the "Select * query is not supported for visualization" toast WAS emitted.
|
||||
*/
|
||||
async expectSelectStarVisualizationToast() {
|
||||
await this.expectToastContaining(this.selectStarNotSupportedToastText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read `quick_mode_enabled` from the instance /config endpoint.
|
||||
* isSelectStarForTable() short-circuits to false when this is off, so the
|
||||
* SELECT * table guard simply does not apply on such an instance.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isQuickModeEnabledOnInstance() {
|
||||
return await this.page
|
||||
.evaluate(async () => {
|
||||
const res = await fetch('/config');
|
||||
if (!res.ok) return false;
|
||||
const cfg = await res.json();
|
||||
return cfg.quick_mode_enabled === true;
|
||||
})
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
// ===== LOGS VISUALIZE — URL STATE =====
|
||||
|
||||
/**
|
||||
* Wait until the URL carries the Visualize tab state, so a subsequent
|
||||
* page.reload() actually exercises URL restoration instead of reloading a URL
|
||||
* that has not been synced yet (updateUrlQueryParams runs asynchronously after
|
||||
* the chart renders).
|
||||
* @param {{ requireVisualizationData?: boolean, requireFunctionContent?: boolean, timeout?: number }} options
|
||||
*/
|
||||
async waitForVisualizeUrlState({
|
||||
requireVisualizationData = true,
|
||||
requireFunctionContent = false,
|
||||
timeout = 20000,
|
||||
} = {}) {
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const url = this.page.url();
|
||||
const hasToggle = url.includes('logs_visualize_toggle=visualize');
|
||||
const hasConfig =
|
||||
!requireVisualizationData || url.includes('visualization_data=');
|
||||
// functionContent is only written when transformType === "function"
|
||||
// AND tempFunctionContent is non-empty, so requiring it proves a VRL
|
||||
// body really is part of the restorable state before we reload.
|
||||
const hasVrl = !requireFunctionContent || url.includes('functionContent=');
|
||||
return hasToggle && hasConfig && hasVrl;
|
||||
},
|
||||
{
|
||||
timeout,
|
||||
message:
|
||||
'URL never picked up logs_visualize_toggle=visualize / visualization_data' +
|
||||
(requireFunctionContent ? ' / functionContent' : ''),
|
||||
}
|
||||
)
|
||||
.toBe(true);
|
||||
testLogger.info('URL carries the visualize tab state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the `visualization_data` URL param (base64-encoded JSON written by
|
||||
* updateUrlQueryParams from dashboardPanelData). After a reload the app
|
||||
* regenerates this param from its restored in-memory state, so comparing the
|
||||
* pre- and post-reload payloads proves the restore actually repopulated the
|
||||
* panel — not merely that the query string survived.
|
||||
* @returns {Promise<object|null>} Decoded payload, or null if absent/unparsable.
|
||||
*/
|
||||
async getVisualizationDataFromUrl() {
|
||||
const raw = new URL(this.page.url()).searchParams.get('visualization_data');
|
||||
if (!raw) return null;
|
||||
try {
|
||||
// URLSearchParams decodes a literal '+' as a space, and standard base64
|
||||
// uses '+', so put those back before decoding.
|
||||
const normalized = raw.replace(/ /g, '+');
|
||||
return JSON.parse(Buffer.from(normalized, 'base64').toString('utf8'));
|
||||
} catch (error) {
|
||||
testLogger.warn(`Could not decode visualization_data: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the VRL (transform) editor currently contains the given text.
|
||||
* @param {string} text - Substring expected in the editor.
|
||||
*/
|
||||
async expectVrlEditorContains(text) {
|
||||
await expect
|
||||
.poll(async () => await this.getVrlEditorContent(), {
|
||||
timeout: 15000,
|
||||
message: `VRL editor never contained "${text}"`,
|
||||
})
|
||||
.toContain(text);
|
||||
testLogger.info(`VRL editor contains "${text}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expect Build tab toggle to be visible
|
||||
*/
|
||||
@@ -9942,70 +10200,50 @@ export class LogsPage {
|
||||
* @returns {Promise<string|null>} The current chart type or null
|
||||
*/
|
||||
async getCurrentChartType() {
|
||||
const chartTypes = ['bar', 'line', 'area', 'area-stacked', 'metric', 'table', 'scatter', 'pie', 'donut', 'h-bar', 'h-stacked', 'stacked', 'heatmap', 'gauge'];
|
||||
|
||||
for (const chartType of chartTypes) {
|
||||
const chartItem = this.page.locator(this.chartTypeItem(chartType)).first();
|
||||
const isVisible = await chartItem.isVisible().catch(() => false);
|
||||
if (isVisible) {
|
||||
const parent = chartItem.locator('..');
|
||||
// Prefer data-selected attribute (ChartSelection.vue exposes it on the <li>).
|
||||
// Fall back to legacy bg-grey-3/5 (framework) and bg-gray-200/400 (Tailwind).
|
||||
const dataSelected = await parent.getAttribute('data-selected');
|
||||
if (dataSelected === 'true') {
|
||||
testLogger.info(`Current chart type detected: ${chartType}`);
|
||||
return chartType;
|
||||
}
|
||||
if (dataSelected === null) {
|
||||
const parentClassList = (await parent.getAttribute('class')) || '';
|
||||
if (
|
||||
parentClassList.includes('bg-grey-3') ||
|
||||
parentClassList.includes('bg-grey-5') ||
|
||||
parentClassList.includes('bg-gray-200') ||
|
||||
parentClassList.includes('bg-gray-400')
|
||||
) {
|
||||
testLogger.info(`Current chart type detected: ${chartType}`);
|
||||
return chartType;
|
||||
}
|
||||
// ChartSelection.vue marks the selected chart on the <li> via
|
||||
// :data-test-selected="item.id" (the inner div carries data-selected="true|false",
|
||||
// so reading data-selected off the <li> always yields null). PanelEditor is
|
||||
// mounted in both the Build and Visualize tabs via v-show, so filter to the
|
||||
// visible instance with offsetParent — same approach as verifyChartTypeSelected.
|
||||
const chartType = await this.page.evaluate(() => {
|
||||
const items = document.querySelectorAll('[data-test-selected]');
|
||||
for (const item of items) {
|
||||
if (item.offsetParent !== null) {
|
||||
return item.getAttribute('data-test-selected');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}).catch(() => null);
|
||||
|
||||
if (chartType) {
|
||||
testLogger.info(`Current chart type detected: ${chartType}`);
|
||||
return chartType;
|
||||
}
|
||||
testLogger.warn('No chart type detected as selected');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any chart type to become selected (theme-aware).
|
||||
* Uses page.waitForFunction for reliable detection of bg-grey-3/bg-grey-5
|
||||
* directly in the DOM, surviving reactive re-renders across tab switches.
|
||||
* Wait for any chart type to become selected and return its id.
|
||||
* ChartSelection.vue sets :data-test-selected="item.id" on the selected <li>
|
||||
* (and only on that one), so the attribute value IS the chart type. The inner
|
||||
* div's data-selected="true|false" lives on a different element, and the legacy
|
||||
* bg-grey and bg-gray classes no longer exist (the selected <li> now uses the
|
||||
* bg-label-chip-url-bg token), so neither is a usable signal here.
|
||||
* @param {number} timeout - Max wait time in ms (default 20000)
|
||||
* @returns {Promise<string|null>} The selected chart type or null if timeout
|
||||
*/
|
||||
async waitForChartTypeStabilized(timeout = 20000) {
|
||||
try {
|
||||
// Use waitForFunction to detect bg-grey-3 or bg-grey-5 on chart selection items
|
||||
// This is more reliable than Playwright locator polling during reactive re-renders
|
||||
// waitForFunction rather than locator polling: the panel re-renders
|
||||
// reactively across tab switches and URL restoration.
|
||||
const result = await this.page.waitForFunction(() => {
|
||||
const items = document.querySelectorAll('[data-test="dashboard-addpanel-chart-selection-item"]');
|
||||
const items = document.querySelectorAll('[data-test-selected]');
|
||||
for (const item of items) {
|
||||
const classes = item.className || '';
|
||||
// Prefer data-selected attribute (ChartSelection.vue), fall back to legacy classes
|
||||
const dataSelected = item.getAttribute('data-selected');
|
||||
const matchesSelected = dataSelected === 'true' ||
|
||||
(dataSelected === null && (
|
||||
classes.includes('bg-grey-3') ||
|
||||
classes.includes('bg-grey-5') ||
|
||||
classes.includes('bg-gray-200') ||
|
||||
classes.includes('bg-gray-400')
|
||||
));
|
||||
if (matchesSelected) {
|
||||
// Found selected item - extract chart type from child data-test attribute
|
||||
const section = item.querySelector('[data-test^="selected-chart-"][data-test$="-item"]');
|
||||
if (section) {
|
||||
const attr = section.getAttribute('data-test');
|
||||
const match = attr.match(/^selected-chart-(.+)-item$/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
// PanelEditor is mounted in both Build and Visualize tabs via
|
||||
// v-show, so skip the hidden copy.
|
||||
if (item.offsetParent !== null) {
|
||||
return item.getAttribute('data-test-selected');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -5,6 +5,12 @@ const logData = require("../../fixtures/log.json");
|
||||
const { ingestTestData: _ingestData } = require('../utils/data-ingestion.js');
|
||||
const { getOrgIdentifier } = require('../utils/cloud-auth.js');
|
||||
|
||||
// VRL body the transform-editor test asserts on. clickVrlEditor() types this
|
||||
// into Monaco itself (via keyboard.type + a model-stability wait), so the value
|
||||
// here must match that helper rather than being typed separately — Monaco
|
||||
// ignores direct .inputarea fills.
|
||||
const VRL_FUNCTION = '.a=2';
|
||||
|
||||
// ----- Helpers -----
|
||||
|
||||
async function ingestTestData(page) {
|
||||
@@ -293,4 +299,291 @@ test.describe("Logs Visualization State Persistence testcases", () => {
|
||||
|
||||
testLogger.info('Toggle group rendering test completed');
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// PR #13244 / issue #12897 — Visualize render + URL-refresh regressions
|
||||
// ---------------------------------------------------------------------------
|
||||
// Defect A: convertPanelData() threw "Please select required fields to render
|
||||
// the chart" for the logs Timechart's *custom-query* panel while its x/y axes
|
||||
// were still being populated asynchronously from the result schema.
|
||||
// Defect B: refreshing the URL on the Visualize tab produced a
|
||||
// "Select * query is not supported for visualization" toast plus permanent
|
||||
// No Data, because the toggle was restored ahead of the stream selection and
|
||||
// nothing re-ran the visualization afterwards.
|
||||
//
|
||||
// Every "no error" check below is sequenced AFTER a deterministic positive
|
||||
// end-state; see the assertion helpers in logsPage.js for why each is safe.
|
||||
// ===========================================================================
|
||||
|
||||
test("should render the Visualize chart without the false required-fields error", {
|
||||
tag: ['@logs-visualize-persistence', '@all', '@logs', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing Visualize renders with no required-fields error (defect A)');
|
||||
|
||||
// 1. Verify Logs table results visible (pre-condition).
|
||||
await pm.logsPage.expectLogsSearchResultVisible();
|
||||
|
||||
// 2. Open the Visualize tab (default chart type — no manual chart selection).
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
|
||||
// 3. Positive end-state: the chart actually paints and No Data is gone.
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 4. The custom-query panel's transient empty x/y must NOT surface the
|
||||
// builder-mode required-fields error, and no error panel at all may show.
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
testLogger.info('Visualize default chart rendered with zero errors');
|
||||
});
|
||||
|
||||
test("should restore chart type, query and chart after refreshing the URL on the Visualize tab", {
|
||||
tag: ['@logs-visualize-persistence', '@all', '@logs', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info("Testing PR repro: match_all('error') → h-bar → URL refresh");
|
||||
|
||||
// 1. Run the reported query.
|
||||
await pm.logsPage.typeQuery("match_all('error')");
|
||||
await pm.logsPage.applyQueryAndWaitForSearchResponse();
|
||||
|
||||
// 2. Open the Visualize tab and let the first render complete.
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 3. Switch the chart type to horizontal bar.
|
||||
await pm.logsPage.selectChartType('h-bar');
|
||||
await pm.logsPage.verifyChartTypeSelected('h-bar');
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 4. Gate on the URL sync — updateUrlQueryParams() runs after the chart type
|
||||
// watcher settles, so reloading earlier would not exercise URL restoration.
|
||||
await pm.logsPage.waitForVisualizeUrlState();
|
||||
|
||||
// Capture the panel payload the app wrote for this configured state.
|
||||
const configBeforeReload = await pm.logsPage.getVisualizationDataFromUrl();
|
||||
expect(configBeforeReload, 'visualization_data should be present before reload').toBeTruthy();
|
||||
expect(configBeforeReload.type).toBe('h-bar');
|
||||
|
||||
// 5. Start recording toasts, then refresh the URL.
|
||||
await pm.logsPage.startToastRecorder();
|
||||
await page.reload();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// 6. Positive end-state after restore: (b) chart type still h-bar and
|
||||
// (c) the chart really renders (not the permanent No Data state).
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.verifyChartTypeSelected('h-bar');
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 6b. Chart CONFIG (not just type) survived. After load the app rewrites
|
||||
// visualization_data from its restored in-memory panel, so an equal payload
|
||||
// proves restoreVisualizationFromUrlOnLoad() repopulated the config — a URL
|
||||
// that merely survived untouched could not produce this.
|
||||
await expect
|
||||
.poll(async () => (await pm.logsPage.getVisualizationDataFromUrl())?.type, {
|
||||
timeout: 20000,
|
||||
message: 'visualization_data was not rewritten after reload',
|
||||
})
|
||||
.toBe('h-bar');
|
||||
const configAfterReload = await pm.logsPage.getVisualizationDataFromUrl();
|
||||
expect(configAfterReload.config).toEqual(configBeforeReload.config);
|
||||
|
||||
// 7. (a) no "Select * query is not supported for visualization" toast, and no
|
||||
// spurious required-fields error / error panel.
|
||||
await pm.logsPage.expectNoSelectStarVisualizationToast();
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
// 8. (d) the query survived the refresh.
|
||||
await pm.logsPage.expectQueryEditorContainsText("match_all('error')");
|
||||
|
||||
testLogger.info('URL refresh restored chart type, query and chart with zero errors');
|
||||
});
|
||||
|
||||
test("should re-render the Visualize chart after reload with the default chart type", {
|
||||
tag: ['@logs-visualize-persistence', '@all', '@logs', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing reload on Visualize tab with the default chart type');
|
||||
|
||||
// 1. Open the Visualize tab and let the first render complete.
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 2. Capture whatever chart type the Timechart auto-selected — the restore must
|
||||
// bring back the same one without any manual chart-type interaction.
|
||||
const defaultChartType = await pm.logsPage.waitForChartTypeStabilized();
|
||||
expect(defaultChartType).toBeTruthy();
|
||||
testLogger.info(`Default chart type before reload: ${defaultChartType}`);
|
||||
|
||||
// 3. Gate on the URL sync, then reload with toast recording active.
|
||||
await pm.logsPage.waitForVisualizeUrlState();
|
||||
await pm.logsPage.startToastRecorder();
|
||||
await page.reload();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// 4. Positive end-state: same chart type, chart repainted, no No-Data state.
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.verifyChartTypeSelected(defaultChartType);
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 5. No error toast and no error panel after the restore.
|
||||
await pm.logsPage.expectNoSelectStarVisualizationToast();
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
testLogger.info('Default chart type re-rendered after reload with zero errors');
|
||||
});
|
||||
|
||||
test("should restore a non-default chart type (area) after refreshing the URL", {
|
||||
tag: ['@logs-visualize-persistence', '@all', '@logs', '@P1']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing chart-type restore is not h-bar specific (area)');
|
||||
|
||||
// 1. Open the Visualize tab and let the first render complete.
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 2. Switch to the area chart ("area" is one of the chart types the fix's
|
||||
// restoreVisualizationFromUrlOnLoad() whitelist accepts).
|
||||
await pm.logsPage.selectChartType('area');
|
||||
await pm.logsPage.verifyChartTypeSelected('area');
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 3. Gate on the URL sync, capture the payload, then reload with toast recording.
|
||||
await pm.logsPage.waitForVisualizeUrlState();
|
||||
const areaConfigBeforeReload = await pm.logsPage.getVisualizationDataFromUrl();
|
||||
expect(areaConfigBeforeReload, 'visualization_data should be present before reload').toBeTruthy();
|
||||
expect(areaConfigBeforeReload.type).toBe('area');
|
||||
|
||||
await pm.logsPage.startToastRecorder();
|
||||
await page.reload();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// 4. Positive end-state: area restored and repainted.
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.verifyChartTypeSelected('area');
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 4b. Config survived the restore, not just the chart type (see test above).
|
||||
await expect
|
||||
.poll(async () => (await pm.logsPage.getVisualizationDataFromUrl())?.type, {
|
||||
timeout: 20000,
|
||||
message: 'visualization_data was not rewritten after reload',
|
||||
})
|
||||
.toBe('area');
|
||||
const areaConfigAfterReload = await pm.logsPage.getVisualizationDataFromUrl();
|
||||
expect(areaConfigAfterReload.config).toEqual(areaConfigBeforeReload.config);
|
||||
|
||||
// 5. No error toast and no error panel after the restore.
|
||||
await pm.logsPage.expectNoSelectStarVisualizationToast();
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
testLogger.info('Area chart type restored after reload with zero errors');
|
||||
});
|
||||
|
||||
test("should keep the Visualize chart rendering after reload with a VRL function applied", {
|
||||
tag: ['@logs-visualize-persistence', '@all', '@logs', '@P1']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing VRL variant of the URL-refresh restore');
|
||||
|
||||
// 1. Enable the transform (VRL) editor. clickVrlEditor() focuses Monaco, clears
|
||||
// it, types VRL_FUNCTION and waits for the model to settle past the editor's
|
||||
// debounce, so tempFunctionContent is populated when it returns. Assert the
|
||||
// body really landed rather than trusting the helper.
|
||||
await pm.logsPage.toggleVrlEditor();
|
||||
await pm.logsPage.clickVrlEditor();
|
||||
await pm.logsPage.expectVrlEditorContains(VRL_FUNCTION);
|
||||
|
||||
// 2. Re-run the search so the VRL function is part of the current query state.
|
||||
await pm.logsPage.applyQueryAndWaitForSearchResponse();
|
||||
|
||||
// 3. Open the Visualize tab and let the first render complete.
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 4. Gate on the URL sync INCLUDING functionContent — that param is only written
|
||||
// when transformType === "function" and the body is non-empty, so reaching
|
||||
// this line proves the reload will genuinely restore a VRL-carrying state.
|
||||
await pm.logsPage.waitForVisualizeUrlState({ requireFunctionContent: true });
|
||||
await pm.logsPage.startToastRecorder();
|
||||
await page.reload();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// 5. Positive end-state: chart repainted after the VRL-carrying restore, and the
|
||||
// VRL body itself came back (useLogs restores tempFunctionContent from the URL).
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
await pm.logsPage.expectVrlEditorContains(VRL_FUNCTION);
|
||||
|
||||
// 6. No error toast and no error panel after the restore.
|
||||
await pm.logsPage.expectNoSelectStarVisualizationToast();
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
testLogger.info('VRL visualize state restored after reload with zero errors');
|
||||
});
|
||||
|
||||
test("should never show the required-fields error while toggling Visualize → Logs → Visualize", {
|
||||
tag: ['@logs-visualize-persistence', '@all', '@logs', '@P1']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing manual toggle round-trip stays free of required-fields errors');
|
||||
|
||||
// 1. First entry into Visualize — chart paints, no errors.
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
// 2. Back to Logs.
|
||||
await pm.logsPage.clickLogsToggle();
|
||||
await pm.logsPage.expectLogsSearchResultVisible();
|
||||
|
||||
// 3. Second entry into Visualize — this rebuild resets the panel fields, which is
|
||||
// exactly the transient empty x/y window that used to throw.
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
testLogger.info('Toggle round-trip completed with zero required-fields errors');
|
||||
});
|
||||
|
||||
test("should not emit the SELECT * toast when opening the Visualize tab with no stream in the URL", {
|
||||
tag: ['@logs-visualize-persistence', '@all', '@logs', '@P2']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing direct Visualize navigation without a stream in the URL');
|
||||
|
||||
// 1. Record toasts from document-start of the upcoming navigation.
|
||||
await pm.logsPage.startToastRecorder();
|
||||
|
||||
// 2. Navigate directly with only the visualize toggle. Logs page state is
|
||||
// Vuex-backed (not localStorage), so a URL without a `stream` param is a
|
||||
// genuine "no stream selected yet" load — the ordering that used to make the
|
||||
// toggle watcher build `select * from "undefined"`.
|
||||
await page.goto(
|
||||
`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}&logs_visualize_toggle=visualize`
|
||||
);
|
||||
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
||||
|
||||
// 3. Positive end-state: the Visualize tab really is the active tab. A No Data
|
||||
// placeholder is legitimate here (there is nothing to chart), so this uses the
|
||||
// tab-content check rather than expectVisualizeChartRendered() — but it is
|
||||
// paired with expectLogsSearchResultNotVisible() so a page that silently failed
|
||||
// to restore the tab cannot pass on the toast-absence assertion alone.
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectLogsSearchResultNotVisible();
|
||||
|
||||
// 4. The SELECT * toast must never have been emitted during that load.
|
||||
await pm.logsPage.expectNoSelectStarVisualizationToast();
|
||||
|
||||
testLogger.info('Direct Visualize navigation emitted no SELECT * toast');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
|
||||
const testLogger = require('../utils/test-logger.js');
|
||||
const PageManager = require('../../pages/page-manager.js');
|
||||
const logData = require("../../fixtures/log.json");
|
||||
const { ingestTestData: _ingestData } = require('../utils/data-ingestion.js');
|
||||
const { getOrgIdentifier } = require('../utils/cloud-auth.js');
|
||||
|
||||
// ============================================================================
|
||||
// SELECT * handling on the logs Visualize tab — PR #13244 / issue #12897.
|
||||
//
|
||||
// The old code blocked SELECT * for EVERY chart type, before the chart type was
|
||||
// even finalised. The fix replaces that with isSelectStarForTable(): only the
|
||||
// table chart is blocked (it renders the raw query columns), while histogram-
|
||||
// based charts (line/bar/area/scatter) render histogram(_timestamp)/count(*)
|
||||
// and legitimately ignore the SELECT columns.
|
||||
//
|
||||
// WHY THIS IS ITS OWN SPEC FILE:
|
||||
// isSelectStarForTable() short-circuits to false unless the SERVER was started
|
||||
// with ZO_QUICK_MODE_ENABLED=true. That flag is read once at process start, so
|
||||
// it cannot vary between tests sharing a backend. CI gives each matrix shard its
|
||||
// own OpenObserve process, so this file is pinned to its own shard in
|
||||
// ci-matrix/ci_matrix.json with quick_mode_enabled: "true"; every other shard
|
||||
// omits the field and keeps the default (false). Keep these tests here rather
|
||||
// than folding them back into logsVisualizePersistence.spec.js, or they will
|
||||
// silently skip.
|
||||
// ============================================================================
|
||||
|
||||
// ----- Helpers -----
|
||||
|
||||
async function ingestTestData(page) {
|
||||
await _ingestData(page);
|
||||
}
|
||||
|
||||
// ----- Test Suite -----
|
||||
|
||||
test.describe("Logs Visualize SELECT * handling 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 page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
||||
await ingestTestData(page);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.goto(
|
||||
`${logData.logsUrl}?org_identifier=${getOrgIdentifier()}`
|
||||
);
|
||||
await pm.logsPage.selectStream("e2e_automate");
|
||||
await pm.logsPage.applyQueryAndWaitForSearchResponse();
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Fail loudly rather than skip: this shard exists precisely to run with quick
|
||||
// mode on, so a false here means the shard is misconfigured (or the matrix
|
||||
// entry was lost) and the coverage has silently evaporated.
|
||||
expect(
|
||||
await pm.logsPage.isQuickModeEnabledOnInstance(),
|
||||
'This spec requires the backend to run with ZO_QUICK_MODE_ENABLED=true — ' +
|
||||
'check the quick_mode_enabled flag on this shard in ci-matrix/ci_matrix.json'
|
||||
).toBe(true);
|
||||
|
||||
testLogger.info('Test setup completed');
|
||||
});
|
||||
|
||||
test("should block a table chart on a SELECT * query and revert the chart type", {
|
||||
tag: ['@logs-visualize-selectstar', '@all', '@logs', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing SELECT * table guard (toast + chart type revert)');
|
||||
|
||||
// 1. SQL mode with an explicit SELECT * — in SQL mode
|
||||
// getEffectiveVisualizeQuery() returns the raw query, so this is
|
||||
// deterministically a select-all (quick mode cannot rewrite it).
|
||||
await pm.logsPage.enableSQLMode();
|
||||
await pm.logsPage.typeQuery('SELECT * FROM "e2e_automate"');
|
||||
await pm.logsPage.applyQueryAndWaitForSearchResponse();
|
||||
|
||||
// Precondition gate: the editor really holds a select-all. Without this, a
|
||||
// failure to enter SQL mode would surface as the confusing "expected a toast,
|
||||
// none emitted" below instead of pointing at the actual cause.
|
||||
await pm.logsPage.expectQueryEditorContainsText('SELECT * FROM');
|
||||
|
||||
// 2. Open Visualize — a histogram chart type is selected by default and must render.
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 3. Record the chart type the panel settled on; the revert must return to it.
|
||||
const typeBeforeTable = await pm.logsPage.waitForChartTypeStabilized();
|
||||
expect(typeBeforeTable, 'a chart type should be selected before switching').toBeTruthy();
|
||||
expect(typeBeforeTable).not.toBe('table');
|
||||
|
||||
// 4. Attempt to switch to the table chart with the recorder armed.
|
||||
await pm.logsPage.startToastRecorder();
|
||||
await pm.logsPage.selectChartType('table');
|
||||
|
||||
// 5. The guard fires: toast shown AND the chart type reverts to the previous one.
|
||||
await pm.logsPage.expectSelectStarVisualizationToast();
|
||||
await pm.logsPage.verifyChartTypeSelected('table', false);
|
||||
await pm.logsPage.verifyChartTypeSelected(typeBeforeTable);
|
||||
|
||||
// 6. The revert must leave a WORKING panel, not a blank one — the guard returns
|
||||
// early after reverting, so this proves the previous chart survived intact.
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
testLogger.info(`SELECT * table guard fired and reverted to "${typeBeforeTable}"`);
|
||||
});
|
||||
|
||||
test("should allow a histogram chart on a SELECT * query without the SELECT * toast", {
|
||||
tag: ['@logs-visualize-selectstar', '@all', '@logs', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing SELECT * is allowed for histogram-based charts');
|
||||
|
||||
// 1. Same SELECT * query that blocks the table chart above.
|
||||
await pm.logsPage.enableSQLMode();
|
||||
await pm.logsPage.typeQuery('SELECT * FROM "e2e_automate"');
|
||||
await pm.logsPage.applyQueryAndWaitForSearchResponse();
|
||||
|
||||
// Precondition gate — ESSENTIAL here. This test's payload assertion is the
|
||||
// ABSENCE of a toast, which passes trivially if the query was never a
|
||||
// select-all in the first place (e.g. SQL mode failed to engage and quick mode
|
||||
// rewrote it to SELECT <fields>). Verifying the editor holds a select-all is
|
||||
// what stops this from becoming a test that can never fail.
|
||||
await pm.logsPage.expectQueryEditorContainsText('SELECT * FROM');
|
||||
|
||||
// 2. Arm the recorder BEFORE entering Visualize — the pre-fix code emitted the
|
||||
// toast on entry, ahead of any chart-type choice, so recording from here is
|
||||
// what makes this fail on the unfixed build.
|
||||
await pm.logsPage.startToastRecorder();
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 3. Explicitly pick a histogram-based chart type and confirm it renders.
|
||||
await pm.logsPage.selectChartType('line');
|
||||
await pm.logsPage.verifyChartTypeSelected('line');
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 4. No SELECT * toast at any point, and no error panel.
|
||||
await pm.logsPage.expectNoSelectStarVisualizationToast();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
testLogger.info('SELECT * rendered on a line chart with no toast');
|
||||
});
|
||||
|
||||
test("should keep a SELECT * line chart rendered after a browser refresh without the SELECT * toast", {
|
||||
tag: ['@logs-visualize-selectstar', '@all', '@logs', '@P0']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing SELECT * + histogram chart survives a URL refresh');
|
||||
|
||||
// 1. SELECT * in SQL mode, same as the tests above.
|
||||
await pm.logsPage.enableSQLMode();
|
||||
await pm.logsPage.typeQuery('SELECT * FROM "e2e_automate"');
|
||||
await pm.logsPage.applyQueryAndWaitForSearchResponse();
|
||||
|
||||
// Precondition gate — the post-refresh "no toast" assertion below is a negative,
|
||||
// so it would pass trivially if this were never a select-all query.
|
||||
await pm.logsPage.expectQueryEditorContainsText('SELECT * FROM');
|
||||
|
||||
// 2. Render a histogram-based chart (allowed for SELECT * by the fix).
|
||||
await pm.logsPage.clickVisualizeToggle();
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
await pm.logsPage.selectChartType('line');
|
||||
await pm.logsPage.verifyChartTypeSelected('line');
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 3. Gate on the URL sync so the reload actually exercises URL restoration,
|
||||
// and capture the payload to compare against the restored state.
|
||||
await pm.logsPage.waitForVisualizeUrlState();
|
||||
const configBeforeRefresh = await pm.logsPage.getVisualizationDataFromUrl();
|
||||
expect(configBeforeRefresh, 'visualization_data should be present before refresh').toBeTruthy();
|
||||
expect(configBeforeRefresh.type).toBe('line');
|
||||
|
||||
// 4. Refresh with the recorder armed — the restore path re-runs the
|
||||
// visualization, which is where a spurious SELECT * toast would surface.
|
||||
await pm.logsPage.startToastRecorder();
|
||||
await page.reload();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// 5. The chart comes back: same type, actually painted, not the No Data state.
|
||||
await pm.logsPage.expectVisualizeTabContentVisible();
|
||||
await pm.logsPage.verifyChartTypeSelected('line');
|
||||
await pm.logsPage.expectVisualizeChartRendered();
|
||||
|
||||
// 6. The SELECT * query itself survived the refresh.
|
||||
await pm.logsPage.expectQueryEditorContainsText('SELECT * FROM');
|
||||
|
||||
// 7. The core assertion: the visualization already rendered successfully, so the
|
||||
// restore must NOT re-emit the SELECT * message. Only the table chart is
|
||||
// guarded, and the restored type is line.
|
||||
await pm.logsPage.expectNoSelectStarVisualizationToast();
|
||||
await pm.logsPage.expectNoRequiredFieldsError();
|
||||
await pm.logsPage.expectNoDashboardErrors();
|
||||
|
||||
// 8. Panel config survived too — after load the app rewrites visualization_data
|
||||
// from its restored in-memory panel, so an equal payload proves the restore
|
||||
// repopulated state rather than the query string merely surviving.
|
||||
await expect
|
||||
.poll(async () => (await pm.logsPage.getVisualizationDataFromUrl())?.type, {
|
||||
timeout: 20000,
|
||||
message: 'visualization_data was not rewritten after refresh',
|
||||
})
|
||||
.toBe('line');
|
||||
const configAfterRefresh = await pm.logsPage.getVisualizationDataFromUrl();
|
||||
expect(configAfterRefresh.config).toEqual(configBeforeRefresh.config);
|
||||
|
||||
testLogger.info('SELECT * line chart restored after refresh with no toast');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user