From 8ef549c3e350ec077d5611a8211a3062c53f5dae Mon Sep 17 00:00:00 2001 From: Neha P <144308844+neha00290@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:34:18 +0000 Subject: [PATCH] test: cover builder saved views applied on the build tab (#14245) --- tests/ui-testing/pages/logsPages/logsPage.js | 33 +++- .../RegressionSet/Logs/logs-bugs.spec.js | 186 ++++++++++++++++++ 2 files changed, 214 insertions(+), 5 deletions(-) diff --git a/tests/ui-testing/pages/logsPages/logsPage.js b/tests/ui-testing/pages/logsPages/logsPage.js index c229b69b97..3a12a29451 100644 --- a/tests/ui-testing/pages/logsPages/logsPage.js +++ b/tests/ui-testing/pages/logsPages/logsPage.js @@ -10074,6 +10074,27 @@ export class LogsPage { testLogger.info('Build tab is active'); } + /** + * Expect the Build tab to be the selected tab. + * OToggleGroupItem binds $attrs onto Reka's ToggleGroupItem, so data-test and the + * data-state it stamps sit on the same element. BuildQueryPage mounts only while + * the tab is selected, so both are checked. + */ + async expectBuildTabSelected(timeout = 15000) { + await expect(this.page.locator(this.buildToggle)).toHaveAttribute('data-state', 'on', { timeout }); + await expect(this.page.locator(this.buildQueryPage)).toBeVisible({ timeout }); + testLogger.info('Build tab is the selected tab'); + } + + /** + * Expect the Logs tab to be the selected tab (BuildQueryPage unmounted). + */ + async expectLogsTabSelected(timeout = 15000) { + await expect(this.page.locator(this.logsToggle)).toHaveAttribute('data-state', 'on', { timeout }); + await expect(this.page.locator(this.buildQueryPage)).toHaveCount(0, { timeout }); + testLogger.info('Logs tab is the selected tab'); + } + /** * Expect Builder mode (Auto mode) to be active */ @@ -10227,10 +10248,12 @@ export class LogsPage { * @param {string} chartId - The chart type ID (e.g., 'bar', 'line', 'metric', 'table') */ async selectChartType(chartId) { - // Use .first() to handle multiple matching elements (e.g., from cached panels) - const chartItem = this.page.locator(this.chartTypeItem(chartId)).first(); + // The Build and Visualize tabs each mount a PanelEditor, so the chart list is in + // the DOM twice; the cached one is zero-size and .first() would resolve to it. + const chartItem = this.page.locator(`${this.chartTypeItem(chartId)}:visible`).first(); - // Click the chart item (tests should check visibility before calling this) + await chartItem.waitFor({ state: 'visible', timeout: 15000 }); + await chartItem.scrollIntoViewIfNeeded(); await chartItem.click(); await this.page.waitForTimeout(500); testLogger.info(`Selected chart type: ${chartId}`); @@ -10241,8 +10264,8 @@ export class LogsPage { * @param {string} chartId - The chart type ID */ async expectChartTypeVisible(chartId) { - const chartItem = this.page.locator(this.chartTypeItem(chartId)).first(); - await expect(chartItem).toBeVisible(); + const chartItem = this.page.locator(`${this.chartTypeItem(chartId)}:visible`).first(); + await expect(chartItem).toBeVisible({ timeout: 15000 }); testLogger.info(`Chart type "${chartId}" is visible`); } diff --git a/tests/ui-testing/playwright-tests/RegressionSet/Logs/logs-bugs.spec.js b/tests/ui-testing/playwright-tests/RegressionSet/Logs/logs-bugs.spec.js index 9f025a295e..0ea8495402 100644 --- a/tests/ui-testing/playwright-tests/RegressionSet/Logs/logs-bugs.spec.js +++ b/tests/ui-testing/playwright-tests/RegressionSet/Logs/logs-bugs.spec.js @@ -37,6 +37,62 @@ const logData = require("../../../fixtures/log.json"); const { ingestTestData, sendRequest, getHeaders, getIngestionUrl, waitForFieldValueSearchable } = require('../../utils/data-ingestion.js'); const { getOrgIdentifier, isCloudEnvironment } = require('../../utils/cloud-auth.js'); +// Auto-derives to a bar chart with X+Y populated, so a saved chart type of "line" +// is provably restored config rather than config re-derived from this query. +const BUILDER_QUERY = + 'SELECT histogram(_timestamp) as "x_axis_1", count(*) as "y_axis_1" FROM "e2e_automate" GROUP BY x_axis_1'; + +/** + * Build a chart on the Build tab and save it as a saved view. + * The view must be saved while the Build tab is open — SearchBar only persists + * buildData when logsVisualizeToggle === 'build'. + */ +async function createBuilderSavedView(pm, page, viewName, chartType) { + await pm.logsPage.enableSqlModeIfNeeded(); + await pm.logsPage.setQueryEditorContent(BUILDER_QUERY); + await pm.logsPage.runQueryAndWaitForResults(); + + await pm.logsPage.clickBuildToggle(); + const loaded = await pm.logsPage.waitForBuildTabLoaded(); + expect(loaded, 'Build tab must finish initializing before saving the view').toBeTruthy(); + + await pm.logsPage.selectChartType(chartType); + await pm.logsPage.verifyChartTypeSelected(chartType); + + // clickSaveViewButton opens the utilities menu itself, so the list dialog is not + // opened here — clickSaveViewButton would only have to close it again. + await pm.logsPage.clickSaveViewButton(); + await page.waitForTimeout(500); + await pm.logsPage.fillSavedViewName(viewName); + await page.waitForTimeout(500); + await pm.logsPage.clickSavedViewDialogSave(); + await page.waitForTimeout(2000); + testLogger.info(`Builder saved view created: ${viewName} (chart type ${chartType})`); +} + +async function applySavedViewByName(pm, page, viewName) { + await pm.logsPage.clickSavedViewsExpand(); + await page.waitForTimeout(500); + await pm.logsPage.clickSavedViewSearchInput(); + await pm.logsPage.fillSavedViewSearchInput(viewName); + await page.waitForTimeout(1000); + await pm.logsPage.waitForSavedViewText(viewName); + await pm.logsPage.clickSavedViewByText(viewName); + await page.waitForTimeout(3000); + testLogger.info(`Applied saved view: ${viewName}`); +} + +async function deleteSavedViewQuietly(pm, page, viewName) { + try { + await pm.logsPage.clickDeleteSavedViewButton(viewName).catch(() => {}); + await page.waitForTimeout(500); + await pm.logsPage.clickConfirmButton().catch(() => {}); + testLogger.info(`Cleaned up saved view: ${viewName}`); + } catch (cleanupError) { + testLogger.debug(`Saved view cleanup skipped: ${viewName}`); + } +} + test.describe("Logs Regression Bug Fixes", () => { // Changed from serial to parallel - tests are independent (each gets own page/PM in beforeEach) test.describe.configure({ mode: 'parallel' }); @@ -1916,6 +1972,136 @@ test.describe("Logs Regression Bug Fixes", () => { }); + // ========================================================================== + // Bug #14228: Builder saved view does not apply when already on the Build tab + // https://github.com/openobserve/openobserve/issues/14228 + // ========================================================================== + test("should apply a builder saved view while already on the build tab", { + tag: ['@bug-14228', '@P1', '@regression', '@logsRegression', '@savedViews', '@queryBuilder'] + }, async ({ page }) => { + testLogger.info('Test: Builder saved view applies without a tab toggle (Bug #14228)'); + test.setTimeout(180000); + + // The "streamslog" prefix lets CI saved-view cleanup reap any leak. + const savedViewName = `streamslog_build_14228_${Date.now()}`; + + try { + await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier() || 'default'}`); + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + await pm.logsPage.selectStream('e2e_automate'); + await page.waitForTimeout(1000); + + await createBuilderSavedView(pm, page, savedViewName, 'line'); + + // Diverge the live builder from the saved view. Without this the assertion + // would pass even unfixed, since the builder would already show "line". + await pm.logsPage.selectChartType('area'); + await pm.logsPage.verifyChartTypeSelected('area'); + await pm.logsPage.expectBuildTabSelected(); + testLogger.info('Builder diverged to area chart before applying the saved view'); + + await applySavedViewByName(pm, page, savedViewName); + + // No tab toggle happened, so BuildQueryPage never remounts — the watcher on + // searchObj.meta.savedBuildConfig is the only thing that can apply the view. + await pm.logsPage.expectBuildTabSelected(); + await pm.logsPage.verifyChartTypeSelected('line'); + await pm.logsPage.verifyChartTypeSelected('area', false); + testLogger.info('✓ PRIMARY CHECK PASSED: builder re-initialized to the saved chart type'); + + const xCount = await pm.logsPage.expectXAxisHasItems(); + const yCount = await pm.logsPage.expectYAxisHasItems(); + expect(xCount, 'Bug #14228: X-axis must stay populated after applying the view').toBeGreaterThan(0); + expect(yCount, 'Bug #14228: Y-axis must stay populated after applying the view').toBeGreaterThan(0); + + } finally { + await deleteSavedViewQuietly(pm, page, savedViewName); + } + + testLogger.info('✓ PASSED: Builder saved view applied in place (Bug #14228)'); + }); + + test("should still apply a builder saved view when toggling in from the logs tab", { + tag: ['@bug-14228', '@P1', '@regression', '@logsRegression', '@savedViews', '@queryBuilder'] + }, async ({ page }) => { + testLogger.info('Test: Toggle-in path still applies builder saved views (Bug #14228 no-regression)'); + test.setTimeout(180000); + + const savedViewName = `streamslog_build_14228_toggle_${Date.now()}`; + + try { + await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier() || 'default'}`); + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + await pm.logsPage.selectStream('e2e_automate'); + await page.waitForTimeout(1000); + + await createBuilderSavedView(pm, page, savedViewName, 'line'); + + await pm.logsPage.clickLogsToggle(); + await pm.logsPage.expectLogsTabSelected(); + testLogger.info('Switched back to the Logs tab'); + + await applySavedViewByName(pm, page, savedViewName); + + // Here the view flips the toggle to build and BuildQueryPage mounts, so + // onMounted applies the config. The watcher must not undo or double-apply it. + await pm.logsPage.expectBuildTabSelected(); + await pm.logsPage.verifyChartTypeSelected('line'); + testLogger.info('✓ CHECK PASSED: toggle-in path still restores the saved chart type'); + + } finally { + await deleteSavedViewQuietly(pm, page, savedViewName); + } + + testLogger.info('✓ PASSED: Toggle-in path unaffected (Bug #14228)'); + }); + + test("should not re-initialize the builder when applying a non-builder saved view", { + tag: ['@bug-14228', '@P2', '@regression', '@logsRegression', '@savedViews', '@queryBuilder'] + }, async ({ page }) => { + testLogger.info('Test: Non-builder saved views do not trigger builder re-init (Bug #14228)'); + test.setTimeout(180000); + + const savedViewName = `streamslog_logs_14228_${Date.now()}`; + + try { + await page.goto(`${logData.logsUrl}?org_identifier=${getOrgIdentifier() || 'default'}`); + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + await pm.logsPage.selectStream('e2e_automate'); + await page.waitForTimeout(1000); + + // Saved from the Logs tab, so the view carries no buildData. + await pm.logsPage.clickRefreshButton(); + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + await page.waitForTimeout(2000); + + await pm.logsPage.clickSaveViewButton(); + await page.waitForTimeout(500); + await pm.logsPage.fillSavedViewName(savedViewName); + await page.waitForTimeout(500); + await pm.logsPage.clickSavedViewDialogSave(); + await page.waitForTimeout(2000); + testLogger.info(`Logs-tab saved view created: ${savedViewName}`); + + await pm.logsPage.clickBuildToggle(); + await pm.logsPage.waitForBuildTabLoaded(); + await pm.logsPage.expectBuildTabSelected(); + + await applySavedViewByName(pm, page, savedViewName); + + // buildData is null, so the watcher's guard must skip it and the view's own + // logs toggle must win. + await pm.logsPage.expectLogsTabSelected(); + await pm.logsPage.expectLogsTableVisible(); + testLogger.info('✓ CHECK PASSED: non-builder view applied with no builder re-init'); + + } finally { + await deleteSavedViewQuietly(pm, page, savedViewName); + } + + testLogger.info('✓ PASSED: Non-builder saved view handled correctly (Bug #14228)'); + }); + test.afterEach(async () => { // Cleanup new stream created by field cache test (e2e_automate is never deleted) if (fieldCacheStreamsToCleanup.length > 0 && pm) {