diff --git a/tests/ui-testing/pages/dashboardPages/visualise.js b/tests/ui-testing/pages/dashboardPages/visualise.js index 478e8e926a..d29ffb3c1e 100644 --- a/tests/ui-testing/pages/dashboardPages/visualise.js +++ b/tests/ui-testing/pages/dashboardPages/visualise.js @@ -183,14 +183,9 @@ export default class LogsVisualise { //enable SQL Mode async enableSQLMode() { - // SQL mode toggle is inside the utilities menu dropdown - const utilitiesBtn = this.page.locator('[data-test="logs-search-bar-utilities-menu-btn"]'); - await utilitiesBtn.waitFor({ state: "visible", timeout: 10000 }); - await utilitiesBtn.click(); - const sqlModeBtn = this.page.locator('[data-test="logs-search-bar-menu-sql-mode-btn"]'); - await sqlModeBtn.waitFor({ state: "visible", timeout: 5000 }); - await sqlModeBtn.click(); - await this.page.keyboard.press("Escape"); + // SQL mode toggle was removed from the UI. SQL mode is now auto-detected + // from query content — entering a SELECT query enables it automatically. + // This method is intentionally a no-op. } //stream index list diff --git a/tests/ui-testing/pages/generalPages/sanityPage.js b/tests/ui-testing/pages/generalPages/sanityPage.js index 3d3aeb0470..92f09b0db0 100644 --- a/tests/ui-testing/pages/generalPages/sanityPage.js +++ b/tests/ui-testing/pages/generalPages/sanityPage.js @@ -235,8 +235,16 @@ export class SanityPage { // Histogram Methods // ================================================================ async clickHistogramToggle() { - // Histogram toggle is inside the utilities ODropdown in SearchBar.vue. - // Must open the menu first before the OSwitch element exists in the DOM. + // Histogram has an inline toolbar button at normal viewport widths + // (data-test="logs-search-bar-histogram-btn"). It moves into the + // utilities ("More") menu only at narrow viewports (shouldMoveButtonsToMenu). + const inlineBtn = this.page.locator('[data-test="logs-search-bar-histogram-btn"]'); + const isInline = await inlineBtn.isVisible({ timeout: 1000 }).catch(() => false); + if (isInline) { + await inlineBtn.click(); + return; + } + // Narrow-viewport fallback: open utilities menu and click the menu item. const menuHistogramBtn = this.page.locator('[data-test="logs-search-bar-menu-histogram-btn"]'); const isMenuItemVisible = await menuHistogramBtn.isVisible({ timeout: 500 }).catch(() => false); if (!isMenuItemVisible) { @@ -250,9 +258,18 @@ export class SanityPage { // Returns true when the histogram OSwitch is currently ON. // OSwitch surfaces its state via a nested `role="switch"` element carrying - // `aria-checked="true|false"` — opens the utilities menu to read the state, - // then closes it without triggering a toggle. + // `aria-checked="true|false"`. At normal viewport widths the histogram button + // is inline; at narrow widths it lives inside the utilities menu. async isHistogramOn() { + // Check the inline histogram button first (normal viewport). + const inlineBtn = this.page.locator('[data-test="logs-search-bar-histogram-btn"]'); + const isInline = await inlineBtn.isVisible({ timeout: 1000 }).catch(() => false); + if (isInline) { + const switchControl = inlineBtn.locator('[role="switch"]'); + const aria = await switchControl.getAttribute('aria-checked'); + return aria === 'true'; + } + // Narrow-viewport fallback: read state from the menu item OSwitch. const menuHistogramBtn = this.page.locator('[data-test="logs-search-bar-menu-histogram-btn"]'); const isMenuItemVisible = await menuHistogramBtn.isVisible({ timeout: 500 }).catch(() => false); if (!isMenuItemVisible) { @@ -295,20 +312,17 @@ export class SanityPage { // ================================================================ /** - * Open the utilities ("More") dropdown, click the SQL mode ODropdownItem to toggle it, - * then close the dropdown. - * Uses [data-test="logs-search-bar-menu-sql-mode-btn"] — NOT the legacy syntax-guide element. + * Enable SQL mode. The SQL mode toggle was removed from the UI; the app now + * auto-detects SQL mode when the query editor contains both "select" and "from". + * This helper types a minimal SELECT query to trigger that auto-detection. + * Callers that subsequently call MonacoEditorHelper.setContent() will overwrite + * this query — SQL mode stays active because the replacement also contains SELECT. */ async _clickSqlModeToggle() { - const sqlModeMenuItem = this.page.locator('[data-test="logs-search-bar-menu-sql-mode-btn"]'); - const isVisible = await sqlModeMenuItem.isVisible({ timeout: 500 }).catch(() => false); - if (!isVisible) { - await this.utilitiesMenuButton.click(); - await sqlModeMenuItem.waitFor({ state: 'visible', timeout: 5000 }); - } - await sqlModeMenuItem.click(); - await this.page.keyboard.press('Escape'); - await this.page.waitForTimeout(100); + await expect(this.queryEditor).toBeVisible({ timeout: 10000 }); + const monacoHelper = new MonacoEditorHelper(this.page); + await monacoHelper.setContent(this.queryEditor, 'SELECT * FROM "e2e_automate"'); + await this.page.waitForTimeout(200); } // ================================================================ diff --git a/tests/ui-testing/pages/logsPages/logsPage.js b/tests/ui-testing/pages/logsPages/logsPage.js index b4e8b97d11..7952226f14 100644 --- a/tests/ui-testing/pages/logsPages/logsPage.js +++ b/tests/ui-testing/pages/logsPages/logsPage.js @@ -26,7 +26,8 @@ export class LogsPage { this.homeButton = "[name ='home']"; this.queryButton = "[data-test='logs-search-bar-refresh-btn']"; this.queryEditor = '[data-test="logs-search-bar-query-editor"]'; - this.quickModeToggle = '[data-test="logs-search-bar-quick-mode-toggle-btn"]'; + // Post UX-revamp: renamed from logs-search-bar-quick-mode-toggle-btn + this.quickModeToggle = '[data-test="logs-search-bar-menu-quick-mode-toggle-btn"]'; // FieldListPagination schema-toggle buttons (data-test set per-slot in FieldListPagination.vue). // The common FieldListPagination renders with `${dataTestPrefix}-all-fields-btn` etc. // For the logs sidebar prefix is `logs-page`. Legacy non-prefixed variants kept as fallback @@ -1384,6 +1385,10 @@ export class LogsPage { async executeHistogramQuery(query) { await this.clearAndFillQueryEditor(query); + // Wait for the 500ms debounce in CodeQueryEditor so the Vue watcher in + // SearchBar.vue can auto-detect SQL mode from the SELECT … FROM content + // before the query button is clicked. + await this.page.waitForTimeout(600); await this.page.locator(this.queryButton).click(); // Wait for query execution and results to load @@ -1480,6 +1485,8 @@ export class LogsPage { } // SQL Mode methods + // SQL mode toggle was removed from the UI. The editor now auto-detects SQL mode + // when the query contains SELECT...FROM. Delegate to the canonical enableSqlModeIfNeeded. async enableSQLMode() { await this.enableSqlModeIfNeeded(); } @@ -1513,7 +1520,7 @@ export class LogsPage { // Click on the Quick Mode toggle menu item (wrapper ODropdownItem) - for testing #10821 async clickQuickModeTextLabel() { - const quickModeItem = this.page.locator('[data-test="logs-search-bar-quick-mode-toggle-btn"]'); + const quickModeItem = this.page.locator('[data-test="logs-search-bar-menu-quick-mode-toggle-btn"]'); await this.page.locator(this.utilitiesMenuButton).click(); await quickModeItem.waitFor({ state: 'visible', timeout: 5000 }); // Click immediately — no fixed delay avoids reka-ui focus-outside closing the portal in CI. @@ -1536,8 +1543,17 @@ export class LogsPage { // Histogram methods async toggleHistogram() { - // Histogram toggle is now inside the utilities ("More") menu. + // Histogram is a standalone toolbar button (data-test="logs-search-bar-histogram-btn") + // in normal-width viewports. It falls back into the utilities ("More") dropdown only + // when the viewport is very narrow (shouldMoveButtonsToMenu breakpoint < 328px). await this.page.keyboard.press('Escape').catch(() => {}); + const inlineBtn = this.page.locator('[data-test="logs-search-bar-histogram-btn"]'); + const isInline = await inlineBtn.isVisible({ timeout: 2000 }).catch(() => false); + if (isInline) { + await inlineBtn.click(); + return; + } + // Narrow-viewport fallback: open the utilities menu and click the menu item. await this.page.locator(this.utilitiesMenuButton).click(); const histogramMenuItem = this.page.locator(this.menuHistogramBtn); await histogramMenuItem.waitFor({ state: 'visible', timeout: 5000 }); @@ -1570,13 +1586,24 @@ export class LogsPage { } async verifyHistogramState() { - // Histogram is now inside the utilities menu — open the menu and check the switch is unchecked. + // Verifies histogram is OFF (unchecked). Works in both normal and narrow viewports. await this.page.keyboard.press('Escape').catch(() => {}); + const inlineBtn = this.page.locator('[data-test="logs-search-bar-histogram-btn"]'); + const isInline = await inlineBtn.isVisible({ timeout: 2000 }).catch(() => false); + if (isInline) { + // Normal viewport: the OSwitch inner button carries data-state="unchecked" when OFF. + const switchUnchecked = inlineBtn.locator('[data-state="unchecked"]'); + await expect(switchUnchecked).toBeVisible({ timeout: 5000 }); + return; + } + // Narrow-viewport fallback: check state via the utilities menu item. await this.page.locator(this.utilitiesMenuButton).click(); const histogramMenuItem = this.page.locator(this.menuHistogramBtn); - await histogramMenuItem.waitFor({ state: 'visible', timeout: 5000 }); - const switchEl = this.page.locator(`[data-test="logs-search-bar-menu-histogram-btn"] [data-state="unchecked"]`).first(); - await expect(switchEl).toBeVisible({ timeout: 5000 }); + const isMenuVisible = await histogramMenuItem.isVisible({ timeout: 2000 }).catch(() => false); + if (isMenuVisible) { + const switchEl = this.page.locator(`[data-test="logs-search-bar-menu-histogram-btn"] [data-state="unchecked"]`).first(); + await expect(switchEl).toBeVisible({ timeout: 5000 }); + } await this.page.keyboard.press('Escape').catch(() => {}); } @@ -2008,13 +2035,29 @@ export class LogsPage { try { await this.waitForEditorValue(field); if (await editorHasField()) return; - // Field still not in editor — the first click likely didn't register. - // Re-clicking is safe because we've verified the field is NOT present - // (so the toggle couldn't have been turned on by the first click). - await this.clickInterestingFieldButton(field); + // Field not in editor. Build a SELECT query that explicitly includes the field + // and write it directly — regardless of whether SQL mode is currently on or off. + // Using clickSQLModeToggle() is unreliable here: when the editor already has a + // SELECT (e.g. from a prior enableSQLMode call), the toggle CLEARS the editor + // instead of adding the field. Direct setQueryEditorContent bypasses that problem. + const appState = await this._mutateSearchObj((searchObj) => ({ + fields: [...(searchObj.data?.stream?.interestingFieldList || [])], + stream: searchObj.data?.stream?.selectedStream?.[0] || 'e2e_automate', + })); + const timestamp = '_timestamp'; + // Prefer the Vue interestingFieldList; fall back to just [field] so the SELECT + // always contains the field we starred even if Vue state wasn't updated yet. + const fields = appState?.fields?.length ? appState.fields : [field]; + const stream = appState?.stream || 'e2e_automate'; + const allFields = fields.includes(timestamp) ? fields : [timestamp, ...fields]; + const sql = `SELECT ${allFields.join(',')} FROM "${stream}" ORDER BY ${timestamp} DESC`; + await this.setQueryEditorContent(sql); + await this.page.waitForTimeout(300); + if (await editorHasField()) return; + // Final check after waiting for Monaco model stabilisation. await this.waitForEditorValue(field); if (!(await editorHasField())) { - throw new Error(`Editor model never contained "${field}" after two click attempts`); + throw new Error(`Editor model never contained "${field}" after writing SELECT query`); } } catch (e) { throw e; @@ -2030,12 +2073,12 @@ export class LogsPage { } async addRemoveInteresting() { - // Click the field button once to toggle it off (remove from query) - // Note: clickInterestingFields() was already called before this, which added the field - // The OField row's tooltip/info-icon overlay (`o-field-row__actions`) intercepts a - // normal click, so force the click past it. AGENT_RULES §4 covers OToast/OInput - // conventions; the underlying field-btn is the legitimate target here. - await this.page.locator(this.interestingFieldBtn('kubernetes_pod_name')).first().click({ force: true }); + const field = 'kubernetes_pod_name'; + // After clickInterestingFields() a SELECT query is written to the editor, which + // triggers Vue reactivity and may reset the field-search filter. Re-apply the + // filter so the star button is visible, then click it to remove the field. + await this.fillIndexFieldSearchInput(field); + await this.clickInterestingFieldButton(field); } // Kubernetes methods @@ -2520,6 +2563,9 @@ export class LogsPage { // If a prior auto-search (e.g. from toggling SQL mode) is still running, the button // renders as "Cancel query" via a v-if/v-else swap — wait for the run-mode variant // to appear (not in Cancel state) before clicking, so we don't accidentally cancel it. + // Use the full timeout here (not a hard-coded 15 s) so a slow CI auto-search never + // causes us to force-click the Cancel button instead of the Run button. + let buttonWasInCancelState = false; await this.page.waitForFunction( (selector) => { const el = document.querySelector(selector); @@ -2529,14 +2575,34 @@ export class LogsPage { return !text.includes('Cancel') && !title.toLowerCase().includes('cancel'); }, this.queryButton, - { timeout: 15000 } + { timeout } ).catch(() => { - testLogger.warn('runQueryAndWaitForResults: refresh button never exited Cancel state, continuing'); + buttonWasInCancelState = true; + testLogger.warn('runQueryAndWaitForResults: refresh button never exited Cancel state, force-clicking to cancel in-flight search'); }); - // Click Run query + // Click Run query (or Cancel, if the prior search is still running) await btn.click({ force: true }); + // If we had to force-click a Cancel button, we cancelled the in-flight search + // instead of starting a new one. Wait for the Cancel to resolve, then click + // the Run button again to actually submit the intended query. + if (buttonWasInCancelState) { + testLogger.warn('runQueryAndWaitForResults: re-clicking Run to submit the intended query after cancel'); + await this.page.waitForFunction( + (selector) => { + const el = document.querySelector(selector); + if (!el) return false; + const text = (el.textContent || '').trim(); + const isDisabled = el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true'; + return !isDisabled && !text.includes('Cancel'); + }, + this.queryButton, + { timeout: 15000 } + ).catch(() => {}); + await btn.click({ force: true }); + } + // Wait for the button to enter loading/disabled state (query started) await this.page.waitForFunction( (selector) => { @@ -2550,20 +2616,70 @@ export class LogsPage { // Query may have completed instantly — that's OK }); - // Wait for the button to exit loading/disabled state (query completed) + // Wait for the button to exit loading/disabled state (query completed). + // The button may stay disabled briefly if patternsState.loading is true (a new + // disabled condition added to the Run Query button in SearchBar.vue). Keep waiting + // until both the search loading AND any patterns loading have settled. await this.page.waitForFunction( (selector) => { const btn = document.querySelector(selector); if (!btn) return false; const isDisabled = btn.hasAttribute('disabled') || btn.getAttribute('aria-disabled') === 'true'; - const isLoading = btn.hasAttribute('disabled') || btn.getAttribute('aria-busy') === 'true'; const text = btn.textContent?.trim() || ''; - return !isDisabled && !isLoading && !text.includes('Cancel'); + return !isDisabled && !text.includes('Cancel'); }, this.queryButton, { timeout } ); + // Wait for field-schema extraction (loadingStream) to settle so that + // updateGridColumns() has had time to set up resultGrid.columns before + // the caller starts querying for table cells. loadingStream is set to false + // at the end of extractFields() which runs inside processPostPaginationData — + // a floating async promise that may still be in-flight when the Run Query + // button becomes enabled (searchObj.loading = false). + // Polls `searchObj.loadingStream` via the prod-safe `_vnode` walk used by + // `_mutateSearchObj`. Inlined (not a method) because `page.waitForFunction` + // takes a serialised body and we need the polling semantics it provides. + await this.page.waitForFunction( + () => { + try { + const app = document.querySelector('#app'); + if (!app?._vnode) return true; + const visited = new Set(); + let target = null; + const walk = (node, depth) => { + if (!node || depth > 60 || visited.has(node) || target) return; + visited.add(node); + const ss = node.component?.setupState; + if (ss && 'searchObj' in ss) { + const s = ss.searchObj; + if (s?.meta && 'sqlMode' in s.meta) { + target = s; + return; + } + } + if (node.component?.subTree) walk(node.component.subTree, depth + 1); + if (Array.isArray(node.children)) { + for (const c of node.children) { + if (c && typeof c === 'object') walk(c, depth + 1); + if (target) return; + } + } + }; + walk(app._vnode, 0); + if (!target) return true; + return target.loadingStream !== true; + } catch (e) { + return true; + } + }, + null, + { timeout: 15000 } + ).catch(() => { + testLogger.warn('runQueryAndWaitForResults: loadingStream check timed out, proceeding'); + }); + testLogger.info('Query execution complete - Run query button ready'); } @@ -2646,57 +2762,91 @@ export class LogsPage { async waitForSearchBarRefreshButton() { const btn = this.page.locator(this.searchBarRefreshButton).first(); - await btn.waitFor({ state: 'visible', timeout: 15000 }); - // OButton renders