Files
openobserve/tests/ui-testing/pages/logsPages/logsQueryPage.js
T
9c32b03b34 test: stabilize alpha1 Logs-Core shard on cloud (quick-mode detection + search-toolbar sync + scheduler-list hidden-button recovery) (#13240)
## What

Stabilizes the **Logs-Core** shard of the alpha1 cloud E2E suite to
green, running
**fully parallel** (`--workers=5`). Test-infra + page-object changes
only; **no product
code** (7 files under `tests/ui-testing/`). 

## Root causes & fixes

### Quick mode / interesting fields (7 tests)
- **`enableQuickModeIfDisabled` treated the wrong element as the
quick-mode signal.** It
returned early whenever the field-list **user-defined-schema toggle
group** was visible —
but `FieldListPagination` renders that group whenever a stream exposes a
UDS (always true
on cloud), independent of `searchObj.meta.quickMode`. The per-field
interesting (star)
buttons are gated purely on `quickMode`, so they never rendered and
every
`ensureFieldIsInteresting` call timed out. Now verifies the **actual**
Quick Mode switch
state via the utilities menu and confirms by waiting for a per-field
interesting button.
- **`sqlModeOrderBy` hung on `waitForLoadState('networkidle')`** —
streaming keeps the
network busy so it never settles; switched to `domcontentloaded` (the
editor assertion is
  the real readiness gate).

### Search-toolbar synchronization (logspage / histogram flakes)
- **`clickRefreshButton` / `clickRefresh` clicked the refresh button
mid-state-swap.** The
Run-query button swaps to a "Cancel query" variant (and briefly
detaches/disables) while a
prior/auto search is in flight; clicking then cancelled that search or
hit a
  not-visible/detached node (`element is not visible`), and a subsequent
`waitForResponse('/_search')` never fired (60s timeout). Both now wait
for the run-mode
button to be **visible AND idle** (not Cancel, not disabled/`aria-busy`)
before clicking —
  no `force`.
- **`clickLiveMode5Sec` + `clickRelative6WeeksButton` popovers shifted
mid-click.** The
refresh-interval dropdown and the date picker are popovers anchored to
the toolbar; while a
search is in flight the toolbar reflows and the popover repositions, so
the target never
reaches Playwright's "stable" state (~45s click timeout) or the click
misses (date range
  stayed "Past 15 Minutes"). Both now wait for the toolbar to settle
(`_waitForQueryButtonIdle`) first; the date preset additionally verifies
its trigger label
  and retries once.

### Histogram error-handling
- **Query-propagation race** — `clearAndFillQueryEditor` fills Monaco
but the value reaches
the store on a debounce, so `clickRefresh()` could run the **previous**
query and the
no-events/error state never appeared. Now waits for the Monaco model to
reflect the new
  query (`waitForQueryEditorValue`) before running.

### Logs-table field management
- **Fixed `waitForTimeout(2000)` in `beforeEach` →
`waitForFieldListReady()`** — the
expandable field rows only render once the stream's fields have indexed
**values**, and
cloud indexing after ingestion lags under parallel load, so the sidebar
was empty
(`fieldCount === 0`, "field expand button not found"). Gates on the
data-backed field list
  (60s, matching the global-setup indexing budget).
- **Severity-colors** read status bars after a fixed 3s → poll until
they render.
- **Case-insensitivity** counted matching fields after a fixed 500ms →
`expect.poll`.
- **Persist-after-refresh (app timing)** — the added column survives
reload only via the
`logFilterField` localStorage entry, written **after** the column
renders; reloading before
that commit lost it (now waits for the localStorage write). The restore
is then
intermittently slow/dropped on the first reload under cloud load, so it
reloads up to 2×
  and polls for the restored column.

### jobSearch — scheduler-list hidden button (app defect, worked around)
- **App defect:** the scheduler list renders under
`v-show="showSearchScheduler"`, and the
logs page **resets that flag whenever the URL query loses the
`action=search_scheduler`
param** (`updateUrlQueryParams` strips it after a search). The "Get
Jobs" button is then in
the DOM but `display:none` and never recovers — the whole jobSearch CRUD
suite timed out.
`_getJobRowIndex` now **re-navigates to the scheduler-list URL**
(restores the param →
re-shows the list) and retries; validated firing and recovering live on
alpha. Written up
  for a frontend fix (`fix/search-scheduler-job-issue`).
- **`validateAddJob` "Job added successfully" toast made best-effort** —
it's transient and
`searchSchedulerSubmit` already awaited the POST (returned the jobId),
so the toast can
auto-dismiss before the assertion runs; creation is already confirmed by
the jobId + the
scheduler-list lookup. Same re-open recovery applied to its Get-Jobs
click.

## Scope / safety

- **7 files:** `Logs` page-objects (`logsPage`, `logsQueryPage`,
`logsPageEP`,
`jobScheduler`) + Logs specs (`logshistogram`, `logsquickmode`,
`logstable`). **No product
  code.**
- **No tests deleted, no `.only`, no unconditional skips, no assertions
removed.**
- Every fix replaces a fixed sleep / wrong assumption with a
deterministic wait on the real
  condition — no `waitForTimeout`, no `force` in the touched paths.

## Validation

- Local on cloud alpha: `logsquickmode` 8/8, `logshistogram` 5/5,
`logspage` 23/23,
`logstable` field-management + persist (`--repeat-each` clean),
jobSearch scheduler-list
  recovery observed firing and recovering live.
- alpha1 Logs-Core CI: green —
https://github.com/openobserve/o2-enterprise/actions/runs/29471282407


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-07-17 07:30:01 +00:00

238 lines
11 KiB
JavaScript

import { expect } from '@playwright/test';
const testLogger = require('../../playwright-tests/utils/test-logger.js');
export class LogsQueryPage {
constructor(page) {
this.page = page;
this.queryEditor = '[data-test="logs-search-bar-query-editor"]';
this.dateTimeButton = '[data-test="date-time-btn"]';
this.relative15MinButton = '[data-test="date-time-relative-15-m-btn"]';
this.refreshButton = '[data-test="logs-search-bar-refresh-btn"]';
this.errorMessage = '[data-test="logs-search-error-state"]';
this.utilitiesMenuButton = '[data-test="logs-search-bar-utilities-menu-btn"]';
this.resetFiltersButton = '[data-test="logs-search-bar-reset-filters-btn"]';
this.resultDetail = '[data-test="logs-search-result-detail-undefined"]';
this.histogramToggle = '[data-test="logs-search-bar-show-histogram-toggle-btn"]';
// OSwitch renders the wrapper data-test on a div and the state on an inner
// <button data-state="checked|unchecked"> — drill into that button. Note: a sibling
// OTooltip grace-area span also carries `data-state="closed"`, so we filter on the
// OSwitch states explicitly to avoid the strict-mode collision.
this.histogramToggleCheckedBtn = '[data-test="logs-search-bar-show-histogram-toggle-btn"] [data-state="checked"]';
this.histogramToggleUncheckedBtn = '[data-test="logs-search-bar-show-histogram-toggle-btn"] [data-state="unchecked"]';
this.sqlModeToggle = '[data-test="logs-search-bar-sql-mode-toggle-btn"]';
this.sqlModeToggleCheckedBtn = '[data-test="logs-search-bar-sql-mode-toggle-btn"] [data-state="checked"]';
this.sqlModeToggleUncheckedBtn = '[data-test="logs-search-bar-sql-mode-toggle-btn"] [data-state="unchecked"]';
this.autoRunDropdownBtn = '[data-test="logs-search-bar-refresh-btn"] ~ button';
this.autoRunToggleItem = '[data-test="logs-search-bar-live-mode-toggle-btn"]';
this._autoQueryEnabledCache = undefined;
}
async setDateTimeFilter() {
await this.page.locator(this.dateTimeButton).click({ force: true });
await this.page.locator(this.relative15MinButton).click({ force: true });
}
async typeQuery(query) {
const queryEditor = this.page.locator(this.queryEditor);
await expect(queryEditor).toBeVisible();
await queryEditor.locator(".inputarea").fill(query);
}
async clickRefresh() {
// Same button as logsPage.clickRefreshButton: it swaps to a "Cancel query" variant
// while a prior / auto search is in flight. A plain click during that window cancels
// the in-flight search instead of starting a new one, so a subsequent
// waitForResponse('/_search') never fires (60s timeout on the histogram tests).
// Wait for the run-mode variant to be visible AND idle before clicking.
const btn = this.page.locator(this.refreshButton);
await btn.waitFor({ state: 'visible', timeout: 15000 });
await this.page.waitForFunction((selector) => {
const el = document.querySelector(selector);
if (!el) return false;
const disabled = el.hasAttribute('disabled')
|| el.getAttribute('aria-disabled') === 'true'
|| el.getAttribute('aria-busy') === 'true';
const text = (el.textContent || '').trim();
const title = (el.getAttribute('title') || '').trim();
const isCancel = text.includes('Cancel') || title.toLowerCase().includes('cancel');
return !disabled && !isCancel;
}, this.refreshButton, { timeout: 30000 });
await btn.click();
}
async clickErrorMessage() {
// Try selectors in priority order:
// logs-search-no-events-found-text — LogsNoEventsState (0 results, no error)
// logs-search-error-state — LogsErrorState (query/backend error)
const candidates = [
this.page.locator('[data-test="logs-search-no-events-found-text"]'),
this.page.locator(this.errorMessage),
];
for (const locator of candidates) {
try {
await locator.waitFor({ state: 'visible', timeout: 10000 });
await locator.click({ force: true });
return;
} catch (e) {
continue;
}
}
throw new Error('No error/no-results message found — checked: logs-search-no-events-found-text, logs-search-error-state');
}
async clickResetFilters() {
// Reset filters button is now directly on the toolbar
await this.page.locator(this.resetFiltersButton).click();
}
async clickNoDataFound() {
// Try data-test selectors in priority order:
// logs-search-no-events-found-text — Index.vue LogsNoEventsState (0 hits, search applied)
// logs-search-no-data-histogram — SearchResult.vue histogram empty state
// logs-search-error-state — Index.vue error banner (SQL parse / backend error)
const locators = [
this.page.locator('[data-test="logs-search-no-events-found-text"]'),
this.page.locator('[data-test="logs-search-no-data-histogram"]'),
this.page.locator('[data-test="logs-search-error-state"]'),
];
for (const locator of locators) {
try {
await locator.waitFor({ state: 'visible', timeout: 10000 });
await locator.click({ force: true });
return;
} catch (e) {
continue;
}
}
throw new Error('No "no data" message found — checked: logs-search-no-events-found-text, logs-search-no-data-histogram, logs-search-error-state');
}
async clickResultDetail() {
await this.page.locator(this.resultDetail).click();
}
async waitForTimeout(milliseconds) {
await this.page.waitForTimeout(milliseconds);
}
async toggleHistogram() {
// In normal viewport the histogram is a standalone toolbar button (inline).
// In narrow viewport it moves into the More menu.
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 utilities menu and click the menu item.
await this.page.locator(this.utilitiesMenuButton).click();
const histogramMenuItem = this.page.locator('[data-test="logs-search-bar-menu-histogram-btn"]');
await histogramMenuItem.waitFor({ state: 'visible', timeout: 5000 });
await histogramMenuItem.click();
}
async isHistogramOn() {
// In normal viewport the histogram is a standalone inline toolbar button.
// Check its OSwitch state directly without opening the More menu.
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) {
const switchChecked = inlineBtn.locator('[data-state="checked"]');
return (await switchChecked.count()) > 0;
}
// Narrow-viewport fallback: check state via the More menu item.
await this.page.locator(this.utilitiesMenuButton).click();
const histogramMenuItem = this.page.locator('[data-test="logs-search-bar-menu-histogram-btn"]');
await histogramMenuItem.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
const switchChecked = this.page.locator('[data-test="logs-search-bar-menu-histogram-btn"] [data-state="checked"]');
const count = await switchChecked.count();
await this.page.keyboard.press('Escape').catch(() => {});
return count > 0;
}
async ensureHistogramState(desiredState) {
const isOn = await this.isHistogramOn();
if (isOn !== desiredState) {
await this.toggleHistogram();
}
}
/** SQL mode toggle was removed from the UI — no-op, always returns false. */
async _openUtilitiesMenuForSqlMode() {
// SQL mode is now auto-detected from query content (SELECT...FROM = SQL ON, else OFF).
// There is no longer a dedicated SQL mode toggle button in the utilities menu.
return false;
}
async isSQLModeOn() {
// SQL mode is auto-detected: if the Monaco editor contains SELECT...FROM, SQL mode is ON.
const text = await this.page.evaluate((selector) => {
const host = document.querySelector(selector);
if (!host) return null;
const editors = window.monaco?.editor?.getEditors?.() ?? [];
for (const ed of editors) {
const node = ed.getDomNode?.();
if (node && host.contains(node)) return ed.getValue();
}
return null;
}, '[data-test="logs-search-bar-query-editor"]');
if (!text) return false;
const lower = text.toLowerCase().trim();
return lower.includes('select') && lower.includes('from');
}
async ensureSQLMode() {
// SQL mode toggle removed from UI. SQL mode is auto-detected from SELECT...FROM in query.
// Callers that need SQL mode should set a SELECT query via the editor after this call.
testLogger.info('ensureSQLMode: SQL mode toggle removed — SQL mode is auto-detected from query content');
}
async ensureFTSMode() {
// SQL mode toggle removed from UI. SQL mode is auto-detected from SELECT...FROM in query.
// Callers that need FTS mode should set a non-SELECT query via the editor after this call.
testLogger.info('ensureFTSMode: SQL mode toggle removed — SQL mode is auto-detected from query content');
}
async _isAutoQueryEnabled() {
if (this._autoQueryEnabledCache !== undefined) return this._autoQueryEnabledCache;
this._autoQueryEnabledCache = await this.page.evaluate(async () => {
try {
const res = await fetch('/config', { credentials: 'include' });
if (!res.ok) return false;
const cfg = await res.json();
return cfg?.auto_query_enabled === true;
} catch {
return false;
}
});
return this._autoQueryEnabledCache;
}
async _toggleAutoRun(expectedLabel) {
if (!(await this._isAutoQueryEnabled())) {
testLogger.info(`auto_query_enabled=false on this env — skipping ${expectedLabel === 'Turn off' ? 'disableAutoRun' : 'enableAutoRun'}`);
return;
}
await this.page.locator(this.autoRunDropdownBtn).first().click();
const toggle = this.page.locator(this.autoRunToggleItem);
await expect(toggle).toBeVisible({ timeout: 5000 });
if (((await toggle.textContent()) || '').includes(expectedLabel)) {
await toggle.click();
// After clicking, the toggle menu closes — wait for it to be detached/hidden.
await toggle.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
} else {
await this.page.keyboard.press('Escape');
await toggle.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
}
}
async disableAutoRun() {
await this._toggleAutoRun('Turn off');
}
async enableAutoRun() {
await this._toggleAutoRun('Turn on');
}
}