mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 00:08:55 +00:00
## 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>
251 lines
12 KiB
JavaScript
251 lines
12 KiB
JavaScript
// cipher keys.js
|
|
import { expect } from '@playwright/test';
|
|
|
|
|
|
export class CipherKeys {
|
|
|
|
constructor(page) {
|
|
|
|
this.page = page;
|
|
|
|
this.settingsMenu = page.locator('[data-test="menu-link-/settings-item"]');
|
|
this.cipherKeyTab = page.locator('[data-test="management-cipher-key-tab"]');
|
|
this.addCipherKeyButton = page.locator('[data-test="cipher-keys-add-btn"]');
|
|
// OInput wrapper: [data-test="add-cipher-key-name-input"]
|
|
// Native <input>: [data-test="add-cipher-key-name-input-field"] (-field suffix, hidden — use { force: true })
|
|
this.nameInput = page.locator('[data-test="add-cipher-key-name-input-field"]');
|
|
// OTextarea wrapper: [data-test="add-cipher-key-openobserve-secret-input"]
|
|
// Native <textarea>: [data-test="add-cipher-key-openobserve-secret-input-field"] (-field suffix)
|
|
this.secretInput = page.locator('[data-test="add-cipher-key-openobserve-secret-input-field"]');
|
|
this.continueButton = page.locator('[data-test="add-report-step1-continue-btn"]');
|
|
this.saveButton = page.locator('[data-test="add-cipher-key-save-btn"]');
|
|
// OToast: inner description div has data-test="o-toast-message" and contains only {{ message }}.
|
|
// OIcon renders as SVG (no text content), so textContent() of o-toast-message = the message string.
|
|
this.alert = page.locator('[data-test="o-toast-message"]').first();
|
|
|
|
this.deleteButton = cipherName => `[data-test="cipherkey-list-${cipherName}-delete"]`;
|
|
this.confirmOkButton = '[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]';
|
|
this.cancelButton = '[data-test="confirm-dialog"] [data-test="o-dialog-secondary-btn"]';
|
|
this.updateButton = cipherName => `[data-test="cipherkey-list-${cipherName}-update"]`;
|
|
this.profileButton = page.locator('[data-test="header-my-account-profile-icon"]');
|
|
this.signOutButton = page.locator('[data-test="header-sign-out-btn"]');
|
|
|
|
|
|
}
|
|
|
|
async navigateToSettingsMenu() {
|
|
// Navigate straight to the Cipher Keys settings sub-route instead of clicking
|
|
// the left-nav Settings menu. The revamped Settings landing is a SectionHub
|
|
// (settings/index.vue) whose per-tab data-tests (e.g. `management-cipher-key-tab`)
|
|
// only render once you are INSIDE a section sub-route — landing on the hub left
|
|
// the tab unrendered, so the old `waitForSelector('[data-test="management-cipher-key-tab"]')`
|
|
// could only exhaust the 45s action timeout. Cipher Keys shares the exact same
|
|
// `visible: isEnt` gate as Regex Patterns (settings/index.vue:236/246), and the
|
|
// SDR shard's regex-management tests pass on this same alpha1 cloud env, proving
|
|
// the tab DOES render here — this mirrors the proven direct-goto that
|
|
// regexPatternsPage.navigateToRegexPatterns() uses.
|
|
const orgName = process.env.ORGNAME || 'default';
|
|
const baseUrl = process.env.ZO_BASE_URL;
|
|
await this.page.goto(`${baseUrl}/web/settings/cipher_keys?org_identifier=${orgName}`);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async navigateToCipherKeyTab() {
|
|
// On the cipher_keys sub-route the tab is the active rail item; assert it is
|
|
// rendered (real readiness signal, bounded — not the 45s action timeout) and
|
|
// click it to guarantee we land on the Cipher Keys list. Idempotent: safe when
|
|
// navigateToSettingsMenu() already placed us on the route.
|
|
await expect(this.cipherKeyTab).toBeVisible({ timeout: 30000 });
|
|
await this.cipherKeyTab.click();
|
|
}
|
|
|
|
async addCipherKey() {
|
|
await this.addCipherKeyButton.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.addCipherKeyButton.click();
|
|
|
|
}
|
|
|
|
async addCipherKeyName(name) {
|
|
// OInput native <input> is hidden from Playwright — use attached + force.
|
|
await this.nameInput.waitFor({ state: 'attached', timeout: 10000 });
|
|
await this.nameInput.fill(name, { force: true });
|
|
}
|
|
|
|
|
|
async addCipherKeyOO(secret) {
|
|
// OTextarea native <textarea> uses -field suffix; may be hidden — use attached + force.
|
|
await this.secretInput.waitFor({ state: 'attached', timeout: 10000 });
|
|
await this.secretInput.fill(secret, { force: true });
|
|
}
|
|
|
|
async addCipherKeyContinue() {
|
|
await this.page.waitForSelector('[data-test="add-report-step1-continue-btn"]');
|
|
await this.continueButton.click();
|
|
}
|
|
|
|
async addCipherKeyTink() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-auth-method-input"]');
|
|
await this.page.locator('[data-test="cipher-key-encryption-mechanism-step"] [data-test="add-cipher-key-auth-method-input"]').click();
|
|
await this.page.locator('[data-test="add-cipher-key-auth-method-input-option"]', { hasText: /Tink KeySet/i }).first().click();
|
|
}
|
|
|
|
async addCipherKeyType() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-type-input"]');
|
|
await this.page.locator('[data-test="add-cipher-key-type-input"]').click();
|
|
await this.page.locator('[data-test="add-cipher-key-type-input-option"]', { hasText: /Akeyless/i }).first().click();
|
|
}
|
|
|
|
async addCipherKeyTypeURL(abase) {
|
|
const field = this.page.locator('[data-test="add-cipher-key-akeyless-baseurl-input-field"]');
|
|
await field.waitFor({ state: 'attached', timeout: 10000 });
|
|
await field.fill(abase, { force: true });
|
|
}
|
|
|
|
async addCipherKeyTypeID(aid) {
|
|
const field = this.page.locator('[data-test="add-cipher-key-akeyless-access-id-input-field"]');
|
|
await field.waitFor({ state: 'attached', timeout: 10000 });
|
|
await field.fill(aid, { force: true });
|
|
}
|
|
|
|
async addCipherKeyTypeKey(akey) {
|
|
const field = this.page.locator('[data-test="add-cipher-key-akeyless-access-key-input-field"]');
|
|
await field.waitFor({ state: 'attached', timeout: 10000 });
|
|
await field.fill(akey, { force: true });
|
|
}
|
|
|
|
async addCipherKeyStatic() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-secret-type-input"]');
|
|
await this.page.locator('[data-test="add-cipher-key-secret-type-input"]').click();
|
|
await this.page.locator('[data-test="add-cipher-key-secret-type-input-option"]', { hasText: /Static Secret/i }).first().click();
|
|
}
|
|
|
|
async addCipherKeyStaticName(astatic) {
|
|
const field = this.page.locator('[data-test="add-cipher-key-akeyless-static-secret-name-input-field"]');
|
|
await field.waitFor({ state: 'attached', timeout: 10000 });
|
|
await field.fill(astatic, { force: true });
|
|
}
|
|
|
|
|
|
async addCipherKeyDFC() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-secret-type-input"]');
|
|
await this.page.locator('[data-test="add-cipher-key-secret-type-input"]').click();
|
|
await this.page.locator('[data-test="add-cipher-key-secret-type-input-option"]', { hasText: /DFC/i }).first().click();
|
|
}
|
|
|
|
async addCipherKeyNameDFC(nameDFC) {
|
|
const field = this.page.locator('[data-test="add-cipher-key-akeyless-dfc-name-input-field"]');
|
|
await field.waitFor({ state: 'attached', timeout: 10000 });
|
|
await field.fill(nameDFC, { force: true });
|
|
}
|
|
|
|
async addCipherKeyEncryDataDFC(encryDFC) {
|
|
// OTextarea -field suffix
|
|
const field = this.page.locator('[data-test="add-cipher-key-akeyless-dfc-encrypted-data-input-field"]');
|
|
await field.waitFor({ state: 'attached', timeout: 10000 });
|
|
await field.fill(encryDFC, { force: true });
|
|
}
|
|
|
|
async addCipherKeySimple() {
|
|
await this.page.locator('[data-test="add-cipher-key-auth-method-input-option"]', { hasText: /Simple/i }).first().click();
|
|
}
|
|
|
|
async addCipherKeyStore() {
|
|
await this.page.locator('[data-test="cipher-key-encryption-mechanism-step"]').click();
|
|
}
|
|
|
|
async addCipherKeySave() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-save-btn"]');
|
|
await this.saveButton.click();
|
|
}
|
|
|
|
async updateCipherKeys(cipherName) {
|
|
const updateButtonLocator = this.page.locator(this.updateButton(cipherName));
|
|
await updateButtonLocator.waitFor({ state: 'visible', timeout: 30000 });
|
|
await updateButtonLocator.click({ force: true });
|
|
|
|
}
|
|
|
|
async updateCipherKeysSecret() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-openobserve-secret-input-update"]');
|
|
await this.page.locator('[data-test="add-cipher-key-openobserve-secret-input-update"]').click();
|
|
|
|
}
|
|
|
|
async updateCipherKeysSecretCancel() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-openobserve-secret-input-cancel"]');
|
|
await this.page.locator('[data-test="add-cipher-key-openobserve-secret-input-cancel"]').click();
|
|
|
|
}
|
|
|
|
async updateCipherKeysCancel() {
|
|
await this.page.waitForSelector('[data-test="add-cipher-key-cancel-btn"]');
|
|
await this.page.locator('[data-test="add-cipher-key-cancel-btn"]').click();
|
|
|
|
}
|
|
|
|
async deletedCipherKeys(cipherName) {
|
|
const deleteButtonLocator = this.page.locator(this.deleteButton(cipherName));
|
|
await deleteButtonLocator.waitFor({ state: 'visible', timeout: 30000 });
|
|
await deleteButtonLocator.click({ force: true });
|
|
|
|
}
|
|
|
|
async requestCipherKeysOk() {
|
|
await this.page.locator(this.confirmOkButton).waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.page.locator(this.confirmOkButton).click({ force: true });
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
async requestCipherKeysCancel() {
|
|
await this.page.locator(this.cancelButton).waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.page.locator(this.cancelButton).click({ force: true });
|
|
await this.page.waitForTimeout(2000);
|
|
}
|
|
|
|
async verifyAlertMessage(expectedText) {
|
|
// Polls ALL visible [data-test="o-toast-message"] elements (loading + error toasts can
|
|
// coexist or appear sequentially). Using .first() would latch onto the loading toast
|
|
// "Please wait..." and miss the subsequent error toast when the API responds.
|
|
const toasts = this.page.locator('[data-test="o-toast-message"]');
|
|
const timeout = 12000; // extra headroom for API round-trip after loading toast appears
|
|
const interval = 300;
|
|
let elapsed = 0;
|
|
let lastSeenTexts = [];
|
|
|
|
const lowerExpected = expectedText.toLowerCase();
|
|
while (elapsed < timeout) {
|
|
const texts = await toasts.allTextContents().catch(() => []);
|
|
lastSeenTexts = texts;
|
|
if (texts.some(t => t && t.toLowerCase().includes(lowerExpected))) {
|
|
return;
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
elapsed += interval;
|
|
}
|
|
|
|
const seen = lastSeenTexts.length > 0
|
|
? `Last visible toasts: ${JSON.stringify(lastSeenTexts)}`
|
|
: 'No toasts were visible at last check.';
|
|
throw new Error(`Expected alert message "${expectedText}" not found within ${timeout}ms. ${seen}`);
|
|
}
|
|
|
|
// OInput inline error: [data-test="<parent>-error"] rendered by OInput when :error=true
|
|
async verifyNameError() {
|
|
const errorEl = this.page.locator('[data-test="add-cipher-key-name-input-error"]');
|
|
await expect(errorEl).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
// OTextarea inline error: role="alert" span inside the wrapper (OTextarea has no -error data-test)
|
|
async verifySecretError() {
|
|
const errorEl = this.page.locator('[data-test="add-cipher-key-openobserve-secret-input"] [role="alert"]');
|
|
await expect(errorEl).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async signOut() {
|
|
await this.profileButton.click();
|
|
await this.signOutButton.click();
|
|
}
|
|
|
|
|
|
}
|