mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
## What & why
Begins consolidating the **correlation (service-streams) e2e suite**
onto the standard `tests/ui-testing` framework. Today those specs live
in a self-contained parallel harness (own configs, own auth, own `O2_*`
env vars, per-test orgs with no teardown, non-standard names) and are
**not registered in any CI matrix** — so they never run automatically.
This is **Phase 0**: land the shared standard-shaped API helper and
convert the first spec as the **reference pattern** for review, before
batch-converting the rest.
## Changes
- **`utils/correlation-api-helpers.js`** (new) — single source of truth
for correlation API plumbing. Auth via `authedRequest`/`getAuthHeaders`
(framework session + 401/403 self-heal), per-test **fresh-org create +
teardown**. No `test`/`expect`.
- **`Correlation/correlation-discovery.spec.js`** (new) — converted from
`journeyA`: `enhanced-baseFixtures`, `@correlation`/`@P0` tags,
Copyright + JSDoc header, `testLogger.testStart`, standard `ZO_*` env
and naming. Assertions ported **1:1** (DISC-01/02/03).
- Remove `Correlation/journeyA-discovery.api.spec.js` (superseded).
## Deliberate deviation (documented)
Correlation discovery / identity config / `_reset` are **org-global and
destructive**, so each test provisions a **fresh org** and tears it down
in `afterEach`. This keeps the isolation the feature genuinely needs
while fixing the real defect in the old suite: **orgs were created but
never deleted** (leak).
## Validation
- `node --check` clean; requires resolve.
- Collects cleanly under `playwright.config.js` (`--list` → 3 tests,
tagged).
- ⚠️ **Green run pending an enterprise env** (service_streams 403s on
OSS). Not wired into CI yet, so `main` stays green. **Draft** until
validated.
## Follow-ups (later phases)
Batch-convert remaining 4 API + 3 UI specs · revive
`correlationSettings.spec.js` against the new UI · register a dedicated
**Correlation** shard in the ENT `ci_matrix.ent.json` · delete
`corr-*.config.js`.
207 lines
6.8 KiB
JavaScript
207 lines
6.8 KiB
JavaScript
// Copyright 2026 OpenObserve Inc.
|
|
|
|
/**
|
|
* Browser-side helpers for the Correlation Phase-2 UI specs.
|
|
*
|
|
* Auth is handled by the framework fixture (enhanced-baseFixtures →
|
|
* global-setup storageState), so there is NO custom login here. `withSetupPage`
|
|
* mints a short-lived authenticated page from the same storageState for
|
|
* expensive one-time beforeAll setup (org create + ingest + discovery wait)
|
|
* without a bespoke Basic-auth context.
|
|
*
|
|
* The behavioral helpers (quick-mode toggle, correlate-traffic sniffer) are
|
|
* ported from the proven pre-standardization corrUi.js. Selectors for the
|
|
* correlation drawer/settings live in their page objects
|
|
* (correlationDrawerPage / correlationSettingsPage); the logs-page interactions
|
|
* below are search-readiness plumbing, kept here alongside the sniffer.
|
|
*/
|
|
|
|
const path = require("path");
|
|
|
|
const BASE = process.env.ZO_BASE_URL || "http://localhost:5080";
|
|
const AUTH_FILE = path.join(__dirname, "auth", "user.json");
|
|
|
|
/**
|
|
* Run `fn` with a short-lived authenticated page (global-setup storageState).
|
|
* For beforeAll/afterAll setup where the test `page` fixture isn't available.
|
|
*/
|
|
async function withSetupPage(browser, fn) {
|
|
const ctx = await browser.newContext({
|
|
storageState: AUTH_FILE,
|
|
viewport: { width: 1500, height: 1024 },
|
|
});
|
|
const page = await ctx.newPage();
|
|
try {
|
|
return await fn(page);
|
|
} finally {
|
|
await ctx.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force quick mode OFF (state-aware — the buttons are toggles). Quick mode
|
|
* starves dimension extraction: the row source carries only "interesting"
|
|
* fields and correlation sees no dimensions at all.
|
|
*/
|
|
async function ensureQuickModeOff(page) {
|
|
const readSwitch = async (loc) => {
|
|
const sw = loc.locator('[role="switch"], input[type="checkbox"]').first();
|
|
const target = (await sw.count()) ? sw : loc;
|
|
const aria = await target.getAttribute("aria-checked").catch(() => null);
|
|
if (aria !== null) return aria === "true";
|
|
return target.isChecked().catch(() => null);
|
|
};
|
|
|
|
const pinned = page
|
|
.locator('[data-test="logs-search-bar-quick-mode-pinned-btn"]')
|
|
.first();
|
|
if (await pinned.isVisible().catch(() => false)) {
|
|
if ((await readSwitch(pinned)) === true) {
|
|
await pinned.click();
|
|
await page.waitForTimeout(600);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const menuToggle = page
|
|
.locator('[data-test="logs-search-bar-menu-quick-mode-toggle-btn"]')
|
|
.first();
|
|
const candidates = [
|
|
page.locator('button:has-text("More")').first(),
|
|
page.locator('[data-test="logs-search-bar-more-btn"]').first(),
|
|
page.locator('[data-test="logs-search-bar-more-options-btn"]').first(),
|
|
];
|
|
let opened = false;
|
|
for (const cand of candidates) {
|
|
if (!(await cand.isVisible().catch(() => false))) continue;
|
|
await cand.click();
|
|
if (
|
|
await menuToggle
|
|
.waitFor({ state: "visible", timeout: 4_000 })
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
) {
|
|
opened = true;
|
|
break;
|
|
}
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForTimeout(300);
|
|
}
|
|
if (!opened) {
|
|
throw new Error("ensureQuickModeOff: could not open the More menu with the quick-mode item");
|
|
}
|
|
const sw = page.locator('[data-test="logs-search-bar-quick-mode-switch"]').first();
|
|
if ((await sw.count()) && (await readSwitch(sw)) === true) {
|
|
await menuToggle.click();
|
|
await page.waitForTimeout(400);
|
|
}
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForTimeout(400);
|
|
}
|
|
|
|
/**
|
|
* Open the logs page for a stream, force quick mode off, run the query, and
|
|
* verify the request SQL is a full `select *` (not the dimension-starved
|
|
* quick-mode `select _timestamp`) — correlation is dead without dimensions.
|
|
*/
|
|
async function openLogsAndQuery(page, org, stream) {
|
|
await page.goto(
|
|
`${BASE}/web/logs?org_identifier=${org}&stream_type=logs&stream=${stream}&period=1h`,
|
|
{ waitUntil: "domcontentloaded" },
|
|
);
|
|
const refresh = page.locator('[data-test="logs-search-bar-refresh-btn"]').first();
|
|
await refresh.waitFor({ state: "visible", timeout: 20_000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
await ensureQuickModeOff(page);
|
|
const sqls = [];
|
|
const handler = (req) => {
|
|
if (req.url().includes("_search") && req.method() === "POST") {
|
|
try {
|
|
const sql = JSON.parse(req.postData() || "{}").query?.sql;
|
|
if (sql) sqls.push(String(sql));
|
|
} catch {}
|
|
}
|
|
};
|
|
page.on("request", handler);
|
|
await refresh.click();
|
|
await page.waitForTimeout(5000);
|
|
page.off("request", handler);
|
|
if (sqls.some((s) => /select\s+\*\s+from/i.test(s) && s.includes(stream))) {
|
|
await page
|
|
.locator('[data-test="logs-search-result-logs-table"] tbody tr')
|
|
.first()
|
|
.waitFor({ state: "visible", timeout: 15_000 });
|
|
return;
|
|
}
|
|
}
|
|
throw new Error(
|
|
`openLogsAndQuery: never saw a 'select *' query for ${stream} — quick mode still on?`,
|
|
);
|
|
}
|
|
|
|
/** Click the first result row and wait for the detail dialog. */
|
|
async function openFirstRowDialog(page) {
|
|
const row = page.locator('[data-test="logs-search-result-logs-table"] tbody tr').first();
|
|
await row.waitFor({ state: "visible", timeout: 20_000 });
|
|
await row.click();
|
|
await page
|
|
.locator('[data-test="dialog-box"]')
|
|
.first()
|
|
.waitFor({ state: "visible", timeout: 15_000 });
|
|
}
|
|
|
|
/**
|
|
* Attach sniffers for correlation traffic. Returns live arrays that fill as
|
|
* requests happen: parsed correlate request bodies, parsed correlate response
|
|
* bodies, and raw _search POST bodies.
|
|
*/
|
|
function sniff(page) {
|
|
const correlateRequests = [];
|
|
const correlateResponses = [];
|
|
const searchBodies = [];
|
|
page.on("request", (req) => {
|
|
if (req.method() !== "POST") return;
|
|
const url = req.url();
|
|
if (url.includes("/_correlate")) {
|
|
try {
|
|
correlateRequests.push(JSON.parse(req.postData() || "{}"));
|
|
} catch {}
|
|
} else if (url.includes("_search")) {
|
|
const body = req.postData();
|
|
if (body) searchBodies.push(body);
|
|
}
|
|
});
|
|
page.on("response", (res) => {
|
|
if (res.url().includes("/_correlate")) {
|
|
res
|
|
.json()
|
|
.then((b) => correlateResponses.push(b))
|
|
.catch(() => {});
|
|
}
|
|
});
|
|
return { correlateRequests, correlateResponses, searchBodies };
|
|
}
|
|
|
|
/** Wait until pred() is truthy (browser-side polling for sniffed traffic). */
|
|
async function waitFor(pred, { deadlineMs = 30_000, intervalMs = 500, label = "condition" } = {}) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < deadlineMs) {
|
|
const v = await pred();
|
|
if (v) return v;
|
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
}
|
|
throw new Error(`correlation-ui-helpers.waitFor: timed out waiting for ${label}`);
|
|
}
|
|
|
|
module.exports = {
|
|
BASE,
|
|
withSetupPage,
|
|
ensureQuickModeOff,
|
|
openLogsAndQuery,
|
|
openFirstRowDialog,
|
|
sniff,
|
|
waitFor,
|
|
};
|