Files
openobserve/tests/ui-testing/playwright-tests/GeneralTests/schema.spec.js
T
Vamsi Karthik RandClaude Opus 4.8 fddf2cf264 test(e2e): stabilize alpha1 SDR, GeneralTests & Metrics shards (stream/pattern-readiness + langTranslation OOM + Reka-table & editor races) (#13259)
## What

Stabilizes the **SDR**, **GeneralTests** and **Metrics** shards of the
alpha1 cloud
E2E suite (`playwright_alpha1.yml`) to green — the last three shards
remaining after
the rest of the suite was greened. Rebased on `main`.

The original failing run had, across these shards: **SDR** 1 hard fail +
1 flake,
**GeneralTests** 21 fails + 2 flakes, **Metrics** a load-sensitive
query-persistence
flake. Root causes were cloud data/UI-readiness races (freshly-created
streams &
regex patterns lagging their list/table; browser OOM under parallelism;
revamped Reka
UI re-rendering mid-interaction), all fixed with deterministic waits on
real signals.

**No `waitForTimeout` as a fix, no `force:true`, no `.first()`/`.nth()`
band-aids, no
retry-count bumps to hide failures, no weakened assertions. No
worker-count,
parallelism, playwright-config or CI/yml changes** — test code only.

## Root cause & fixes

**SDR**

1. **`scrollIntoViewIfNeeded` 45s timeout** (`multipleSDRPatterns`,
`queryTimeDrop`).
The revamped Reka streams/schema table re-renders rows mid-animation, so
the
scroll's stability polling spun to the 45s action timeout even though
the element
was visible. Guard with `expect(el).toBeVisible()` + an instant native
`scrollIntoView`; removed the trailing sleeps.
(`streamAssociationPage.js`)

2. **Stream Detail `click()` 45s timeout.** One-shot JS-then-Playwright
click raced the
re-rendering row. Replaced with a `toPass` retry that re-resolves the
button fresh,
   fires a native DOM click, and verifies the detail sidebar opened.

3. **Regex-pattern create→list eventual consistency** (6 combined tests:
`ingestionTime*`,
`queryTime*`). Create is accepted (`verifyPatternCreatedSuccess` passes)
before it
   propagates to the list read-replica, so the UI text-count check threw
"not found in list". Gate on the backend source of truth:
`waitForPatternCreated`
   polls `GET /re_patterns` until present then confirms the UI rendered;
`waitForPatternDeleted` fixes the symmetric "Unable to delete … Reason:
unknown"
   delete race. (`sdrPatternsPage.js`)

4. **Freshly-ingested stream lags the `/streams` table**
(`multipleSDRPatterns` @slow).
`openStreamDetail` reloads + re-searches until the stream's row appears
(bounded),
instead of a one-shot 5s wait. (Local-caught gotcha:
`isVisible({timeout})` does
   **not** auto-wait — switched to `waitFor({state:'visible'})`.)

5. Transient cloud-gateway **connection timeouts** on `page.request`
pattern calls wrapped
   in a connection-error-only retry.

**GeneralTests**

6. **cipherKeys ×6 — tab never visible.** Cipher Keys shares the exact
`visible: isEnt`
gate as Regex Patterns (`settings/index.vue`), and SDR's regex
management passes on
this same cloud env — so the tab **does** render; the test was clicking
the Settings
menu, which lands on the SectionHub where per-tab data-tests aren't
rendered. Fixed
by a **direct `goto('/web/settings/cipher_keys')`**, mirroring the
working regex page.
   (Not skipped — that would have masked a real, fixable nav bug.)

7. **enrichment ×7 — `pipeline-section-tab-enrichmentTables` 45s
force-click.** The tab
only mounts after the pipelines page header renders; the
flyout→pipelines→section-tab
nav is fragile under load. Replaced with direct SPA nav to the
enrichment-tables route
+ `goto` fallback + a `toPass` settle-retry Add-button click.
(`pipelinesPage.js`)

8. **languageTranslation ×8 + landingPage ×1 — browser OOM cascade**
("Target page/context/
browser has been closed"). 10 exhaustive ~27-page full-app crawls ran
`mode:parallel`
under `--workers=5`; a worker's browser was OOM-killed and every test
after it
cascaded. **Kept parallel** (serializing overran the shard timeout) —
the crawl now
navigates `waitUntil:'domcontentloaded'` + a bounded settle instead of
waiting up to
10–30s for `networkidle` on chart/websocket-heavy pages, cutting each
heavy page's
peak-memory dwell ~10s→~3s (so 5 concurrent crawls no longer spike
together) and making
each crawl ~3–4× faster. Added fail-fast on a closed page/context so any
residual crash
   surfaces at its true origin instead of being swallowed/retried.

9. **schema ×2 — `selectStream` can't find just-created
`test1`/`test3`.** Local reproduction
proved the root cause: the **fixed** stream names hit `400 "stream is
being deleted"`
(a prior run's cleanup still finishing) → the stream got no data → never
listed. Fixed
with **unique per-invocation stream names**; also hardened the ingestion
helper
(status-check + transient retry) and added a streams-API readiness gate
before
   `selectStream`. (`schema.spec.js`)

10. **enrichment `@upload` ×2 — `_search` `waitForResponse` timeout.**
The 30s search wait
was armed **before** the up-to-120s `selectStream`, so it expired
mid-selection. Re-armed
it (200-gated) immediately before `applyQuery`, plus a streams-API
readiness gate for
`e2e_automate`. `schemaload` gated on the `streams?type=logs` response
before reload.

**Metrics**

11. Replaced `waitForTimeout`s in `metricsPage.js` with real signals
(`data-selected="true"`,
`data-state="on"`) and fixed a reka `OToggleGroupItem` state-detection
bug (active state
    is `data-state="on"`, not a `selected` class).

12. **`metrics-promql-query-persistence` load-sensitive flake.**
`selectFirstStream` returned
before the async auto-populate finished, and persistence tests read the
query editor
before Monaco re-rendered after tab switches. Added editor-readiness
gates on real
    signals (auto-populated query present; Monaco mounted + laid out) in
    `metricsQueryEditorPage.js`; assertions unchanged.

## Validation

- **CI — SDR / GeneralTests / Metrics green (alpha cloud):**
  https://github.com/openobserve/o2-enterprise/actions/runs/29633444186
- **Original failing run:**
https://github.com/openobserve/o2-enterprise/actions/runs/29481437880
- **Local (alpha cloud, CI conditions, single-worker):** each
previously-failing test re-run
  to green — `schema.spec.js` 3/3, `multipleSDRPatterns.spec.js` 2/2.

## Notes

- 21 test files change (8 page objects + 13 specs). No shared
cleanup/setup, config, or
  workflow files touched.
- RegressionSet was unaffected by this branch — a transient hang in the
workflow's
"pre-test cleanup" step (before any test runs), unrelated to these
changes.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 05:45:02 +00:00

260 lines
12 KiB
JavaScript

const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const PageManager = require('../../pages/page-manager.js');
const testLogger = require('../utils/test-logger.js');
const logsdata = require("../../../test-data/logs_data.json");
const { getAuthHeaders, getOrgIdentifier } = require('../utils/cloud-auth.js');
test.describe.configure({ mode: 'parallel' });
test.describe("Schema testcases", () => {
let pm;
const streamName = `stream${Date.now()}`;
// Ingestion helper function
async function ingestion(page, testStreamName, maxRetries = 5) {
testLogger.debug('Starting ingestion for test stream', { testStreamName });
const orgId = getOrgIdentifier();
const headers = {
...getAuthHeaders(),
"Content-Type": "application/json",
};
// Retry until the ingest is ACCEPTED (HTTP 200). The stream names are now
// unique-per-invocation (test1_<uniq> etc.) so a POST no longer collides with a
// prior run's still-deleting "test1/2/3" stream — the local run proved that
// fixed names returned 400 "stream is being deleted" for ~45s per attempt.
// This retry now only guards genuine transient cloud errors (connection blip /
// 5xx); the old single-shot POST swallowed failures, leaving the stream with no
// data so it never listed and the readiness gate / selectStream timed out.
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const response = await page.evaluate(async ({ url, headers, orgId, streamName, logsdata }) => {
try {
const fetchResponse = await fetch(`${url}/api/${orgId}/${streamName}/_json`, {
method: 'POST',
headers: headers,
body: JSON.stringify(logsdata)
});
const contentType = fetchResponse.headers.get('content-type');
const body = (contentType && contentType.includes('application/json'))
? await fetchResponse.json()
: { error: await fetchResponse.text() };
return { status: fetchResponse.status, body };
} catch (e) {
return { status: 0, body: { error: String((e && e.message) || e) } };
}
}, {
url: process.env.INGESTION_URL,
headers: headers,
orgId: orgId,
streamName: testStreamName,
logsdata: logsdata
});
testLogger.debug('Ingestion response', { attempt, response });
if (response.status === 200) {
return; // accepted — stream will index and become listable
}
const msg = JSON.stringify(response.body || {});
const retriable = response.status === 0
|| response.status >= 500
|| /being deleted|Connection|timeout|ECONNRESET|socket hang up/i.test(msg);
if (!retriable || attempt === maxRetries) {
throw new Error(`Ingestion into "${testStreamName}" failed after ${attempt} attempt(s): status=${response.status} body=${msg}`);
}
testLogger.warn(`Ingestion into ${testStreamName} not accepted (status ${response.status}) — retrying`, { attempt });
await page.waitForTimeout(attempt * 2000);
}
}
/**
* Backend-readiness gate: poll the streams list API until the freshly-created
* stream is actually listed by the backend.
*
* WHY: On cloud under load (--workers=5), a just-ingested stream is not
* immediately visible in the logs stream-list dropdown due to streams-list
* indexing/consistency lag. logsPage.selectStream() reloads the logs page 5x
* internally, but if the backend still hasn't listed the stream those retries
* are exhausted and the test fails with
* `selectStream: Failed to find stream "..." after N attempts`.
*
* This gates on the REAL readiness signal the dropdown itself consumes
* (GET /api/{org}/streams?type=logs -> body.list contains the stream), so the
* subsequent selectStream() succeeds deterministically instead of racing.
* Not a blind retry bump.
*
* @param {import('@playwright/test').Page} page
* @param {string} targetStreamName stream that must appear in the list
*/
async function waitForStreamListed(page, targetStreamName) {
const orgId = getOrgIdentifier();
// Use the SAME base the app's own stream-readiness check uses
// (logsPage.waitForStreamAvailable: INGESTION_URL || ZO_BASE_URL) so we poll
// the exact host/endpoint that feeds selectStream's dropdown. On cloud these
// two hosts differ, so matching the canonical order avoids hitting the wrong one.
const rawBase = process.env.INGESTION_URL || process.env.ZO_BASE_URL || '';
const baseUrl = rawBase.endsWith('/') ? rawBase.slice(0, -1) : rawBase;
const streamsUrl = `${baseUrl}/api/${orgId}/streams?type=logs`;
const headers = getAuthHeaders();
// Bounded poll: ~60s worst case (20 attempts x 3s backoff), well above the
// observed consistency lag while still failing fast if the stream is truly
// never created. Transient network errors are tolerated and retried.
const maxAttempts = 30; // ~90s: freshly-ingested streams can lag the streams-list under cloud load
const backoffMs = 3000;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let listed = false;
try {
const response = await page.request.get(streamsUrl, {
headers,
timeout: 15000,
});
if (response.ok()) {
const body = await response.json().catch(() => null);
const list = (body && body.list) || [];
listed = list.some((s) => s && s.name === targetStreamName);
} else {
testLogger.debug('waitForStreamListed non-OK response', {
status: response.status(),
targetStreamName,
attempt,
});
}
} catch (error) {
// Transient Connection timeout / ECONNRESET etc. — retry.
testLogger.debug('waitForStreamListed request error (will retry)', {
error: error.message,
targetStreamName,
attempt,
});
}
if (listed) {
testLogger.info('Stream is listed by backend — safe to selectStream', {
targetStreamName,
attempt,
});
return;
}
if (attempt < maxAttempts) {
// Documented consistency backoff (streams-list indexing lag on cloud).
await page.waitForTimeout(backoffMs);
}
}
throw new Error(
`waitForStreamListed: stream "${targetStreamName}" was not listed by ` +
`GET ${streamsUrl} after ${maxAttempts} attempts (~${(maxAttempts * backoffMs) / 1000}s). ` +
`Backend never made the ingested stream available.`
);
}
test('stream schema settings updated to be displayed under logs', async ({ page }, testInfo) => {
const testStreamName = `test1_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
// Initialize test setup
testLogger.testStart(testInfo.title, testInfo.file);
// Navigate to base URL with authentication
await navigateToBase(page);
pm = new PageManager(page);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
// Perform ingestion for this test
await ingestion(page, testStreamName);
await page.waitForLoadState('domcontentloaded');
// Navigate to logs page with VRL editor enabled
await pm.logsPage.navigateToLogs(getOrgIdentifier());
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
// Backend-readiness gate: ensure the just-ingested stream is actually listed
// by the streams API before selecting it, avoiding streams-list lag races.
await waitForStreamListed(page, testStreamName);
await pm.logsPage.selectStream(testStreamName);
const allsearch = page.waitForResponse(`**/api/${getOrgIdentifier()}/_search**`, { timeout: 60000 });
await pm.schemaPage.applyQuery();
await allsearch;
// Start of actual test - simplified schema workflow
await pm.schemaPage.completeStreamSettingsSchemaWorkflow(testStreamName);
// Verify schema workflow completed by checking no error messages are present
await pm.schemaPage.verifyNoErrorMessages();
testLogger.assertion('Schema workflow completed without errors', true);
testLogger.info('Stream schema settings test completed');
});
test('should display stream details on navigating from blank stream to stream with details', async ({ page }, testInfo) => {
const testStreamName = `test2_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
// Initialize test setup
testLogger.testStart(testInfo.title, testInfo.file);
// Navigate to base URL with authentication
await navigateToBase(page);
pm = new PageManager(page);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
// Perform ingestion for this test
await ingestion(page, testStreamName);
await page.waitForLoadState('domcontentloaded');
// Navigate to logs page with VRL editor enabled
await pm.logsPage.navigateToLogs(getOrgIdentifier());
// Backend-readiness gate: ensure the just-ingested stream is actually listed
// by the streams API before selecting it, avoiding streams-list lag races.
await waitForStreamListed(page, testStreamName);
await pm.logsPage.selectStream(testStreamName);
const allsearch2 = page.waitForResponse(`**/api/${getOrgIdentifier()}/_search**`, { timeout: 60000 });
await pm.schemaPage.applyQuery();
await allsearch2;
// Start of actual test - complete blank to detailed stream workflow
await pm.schemaPage.completeBlankToDetailedStreamWorkflow(streamName, testStreamName);
// Verify navigation workflow completed by checking streams page is accessible
await pm.schemaPage.verifyStreamsPageAccessible();
testLogger.assertion('Stream navigation workflow completed successfully', true);
});
test('should add a new field and delete it from schema', async ({ page }, testInfo) => {
const testStreamName = `test3_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
// Initialize test setup
testLogger.testStart(testInfo.title, testInfo.file);
// Navigate to base URL with authentication
await navigateToBase(page);
pm = new PageManager(page);
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
// Perform ingestion for this test
await ingestion(page, testStreamName);
await page.waitForLoadState('domcontentloaded');
// Navigate to logs page with VRL editor enabled
await pm.logsPage.navigateToLogs(getOrgIdentifier());
// Backend-readiness gate: ensure the just-ingested stream is actually listed
// by the streams API before selecting it, avoiding streams-list lag races.
await waitForStreamListed(page, testStreamName);
await pm.logsPage.selectStream(testStreamName);
const allsearch3 = page.waitForResponse(`**/api/${getOrgIdentifier()}/_search**`, { timeout: 60000 });
await pm.schemaPage.applyQuery();
await allsearch3;
// Start of actual test - use original working enhanced framework method
await pm.schemaPage.completeAddAndDeleteFieldWorkflow(testStreamName);
testLogger.assertion('Add/delete field workflow completed successfully', true);
});
});