Files
openobserve/tests/ui-testing/playwright-tests/utils/global-setup.js
T
Ashish KolheandShrinath Rao 735dc7d58d feat(traces): restore in-page Service Graph / Service Catalog tabs alongside standalone routes (#13698)
## What

#13659 converted Service Graph and Services Catalog from in-page tabs on
the Traces page into standalone routes reached from the nav-rail flyout.
This PR restores the in-page tabs as a **second access path** — both now
coexist, with distinct semantics:

| Path | Semantics |
|---|---|
| Toolbar tabs on `/traces` | In-page view switch (`?tab=service-graph`
/ `?tab=services-catalog`), shares the page's stream + time context —
the pre-#13659 UX, including old `?tab=` bookmarks |
| Nav-rail flyout | Navigates to the standalone routes
`/traces/service-graph`, `/traces/services` — unchanged from #13659 |

Both paths render the same inner components (`ServiceGraph.vue`,
`ServicesCatalog.vue`); only toolbar chrome differs.

## How

A guided **reverse-apply of #13659's diff to `Index.vue` +
`SearchBar.vue` only** (verified byte-identical to the pre-PR files —
both were untouched on main since), with four deliberate adaptations:

- **Re-added `applyHandoffFilter`** + its two hook call sites, which the
revert would have deleted — the standalone pages' "View Traces" handoff
(`?filter=` landing) depends on it. Covered by a new unit regression
test and verified live.
- The restored in-page `view-traces` handlers call
`viewTracesHandoff.ts` (`buildViewTracesFilter` /
`normalizeViewTracesPayload`) instead of resurrecting the inline filter
builders that #13659 extracted from them — no re-duplication.
- **Fixed a latent pre-#13659 bug**: the graph tab's refresh button
called `serviceGraphRef?.loadServiceGraph()`, which was never in
`ServiceGraph.vue`'s `expose()` — now `refresh()`.
- **Flyout highlight now follows the visible view** on both paths: new
`SubnavChild.activeOnTab` alias marks the Service Graph / Service
Catalog flyout items active when `/traces?tab=…` shows their view
in-page. Previously the same view highlighted different flyout items
depending on how you reached it — the only submenu that deviated.

Also:
- Renamed the "Services" submenu / page header / route title to
**"Service Catalog"**, matching the toolbar tab label (single
`menu.services` locale value covers all).
- `global-setup.js` login verification moved to `navbar-main-nav` — the
same stale-selector fix #13659 applied to `enhanced-baseFixtures.js`;
suites fail setup on main without it.

## Tests

| Suite | Result |
|---|---|
| Unit: `SearchBar` 69, `Index` 76, `ONavGroup` 18 (5 new `activeOnTab`
cases), `viewTracesHandoff` 21, `navGroups` 28 | all pass (233 total, 2
pre-existing skips) |
| E2E (new) `Traces/traces-toolbar-tabs.spec.js` — in-page tab switches,
shared-datetime carry, `?tab=` bookmark reload, flyout→standalone-route
coexistence | 4/4 vs live enterprise build |
| E2E `Traces/service-catalog.spec.js` (standalone-route regression) |
18 pass |
| `type-check`, `eslint` (0 errors), `prettier`, `lint:design:strict` |
clean |

Not run: `Traces/service-graph.spec.js` — its `beforeAll` ingests via
`ZO_BASE_URL`, which requires a single-origin instance (CI); the dev
vite+backend split can't satisfy it. Nothing in this PR touches
`ServiceGraph.vue` or the standalone views.

## Notes for review

- `Index.vue` / `SearchBar.vue` restored regions are intentionally
verbatim pre-#13659 (including its 16 pre-existing lint *warnings*) —
deviations are limited to the four adaptations above, so the
diff-vs-pre-PR is auditable.
- Two known-inert quirks restored deliberately, not bugs to fix here:
`hide-splitter-separator` has no CSS rule (was a no-op pre-#13659 too),
and `@update:activeTab` writes to a getter-only computed.

---------

Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-08-07 12:48:14 +00:00

252 lines
9.5 KiB
JavaScript

const { chromium } = require('@playwright/test');
const path = require('path');
const fs = require('fs');
const logsdata = require("../../../test-data/logs_data.json");
const testLogger = require('./test-logger.js');
const metricsIngestion = require('./metrics-ingestion.js');
const { ingestTraces } = require('./trace-ingestion.js');
const { ingestRumErrors } = require('./rum-error-ingestion.js');
/**
* Global setup for all tests - handles authentication and test data ingestion
* This runs once before all tests start
*/
async function globalSetup() {
testLogger.info('Starting global setup for all tests');
// Create auth storage directory
const authDir = path.join(__dirname, 'auth');
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true });
}
const authFile = path.join(authDir, 'user.json');
// Launch browser and create context
const browser = await chromium.launch({
args: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []
});
const context = await browser.newContext({
viewport: { width: 1500, height: 1024 }
});
const page = await context.newPage();
try {
testLogger.info('Performing global login authentication');
// Navigate to login page with org identifier to establish correct org context
const loginUrl = `${process.env["ZO_BASE_URL"]}?org_identifier=${process.env["ORGNAME"]}`;
await page.goto(loginUrl);
testLogger.debug('Navigated to base URL with org', { url: loginUrl });
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
// Debug: Log current URL and page title
const currentUrl = page.url();
const pageTitle = await page.title();
testLogger.debug('Page loaded', { currentUrl, pageTitle });
// Handle internal user login if needed — data-test only, no text matching.
const internalUserButton = page.locator('[data-test="login-as-internal-user"]');
if (await internalUserButton.count()) {
if (await internalUserButton.first().isVisible().catch(() => false)) {
await internalUserButton.first().click();
testLogger.debug('Clicked internal user login button');
await page.waitForLoadState('domcontentloaded');
}
}
// Fill login credentials — data-test only. OInput's WRAPPER carries
// `data-test="<name>"`; the actual <input>/<textarea> element inside it
// carries `data-test="<name>-field"`. Playwright's fill() requires a
// fillable element so we target the -field suffix directly.
const userIdField = page.locator('[data-test="login-user-id-field"]');
const passwordField = page.locator('[data-test="login-password-field"]');
const signInButton = page.locator('[data-test="login-sign-in"]');
testLogger.debug('Waiting for login fields to appear...');
try {
await userIdField.waitFor({ state: 'visible', timeout: 15000 });
} catch (e) {
await page.screenshot({ path: path.join(__dirname, 'debug-login-page.png') });
const html = await page.content();
testLogger.error('Login field not found', {
url: page.url(),
bodyPreview: html?.substring(0, 500),
});
throw new Error(`Login page did not load correctly. Current URL: ${page.url()}`);
}
await userIdField.fill(process.env["ZO_ROOT_USER_EMAIL"]);
await passwordField.waitFor({ state: 'visible' });
await passwordField.fill(process.env["ZO_ROOT_USER_PASSWORD"]);
await signInButton.waitFor({ state: 'visible' });
await signInButton.click();
testLogger.debug('Login credentials submitted');
// Wait for login to complete — look for navigation or success indicators.
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
// Verify login success by checking for the main nav rail — the current
// rail no longer renders per-route `menu-link-*` items (see #13659, which
// made the same fix in enhanced-baseFixtures.js).
await page.locator('[data-test="navbar-main-nav"]').waitFor({
state: 'visible',
timeout: 10000,
});
testLogger.info('Global login authentication successful');
// Save authentication state
await context.storageState({ path: authFile });
testLogger.info('Authentication state saved successfully', { authFile });
// Skip data ingestion for cleanup-only runs
// Use precise regex to match only cleanup.spec.js (not some-cleanup.spec.js or cleanup.spec-backup.js)
const isCleanupOnly = process.argv.some(arg => /cleanup\.spec\.(js|ts)$/.test(arg));
if (isCleanupOnly || process.env.SKIP_INGESTION === 'true') {
testLogger.info('Skipping data ingestion (cleanup-only run or SKIP_INGESTION=true)');
} else {
// Perform global test data ingestion
testLogger.info('Starting global test data ingestion');
await performGlobalIngestion(page);
testLogger.info('Global test data ingestion completed');
// Skip global metrics ingestion - it's now handled in test-specific beforeAll hooks
// This avoids 404 errors when OpenObserve metrics endpoint isn't ready during global setup
// await performMetricsIngestion();
testLogger.debug('Metrics ingestion skipped in global setup - will be handled in test beforeAll hooks');
// Perform trace data ingestion
testLogger.info('Starting trace data ingestion');
await ingestTraces(page, 20); // Ingest 20 test traces
testLogger.info('Trace data ingestion completed');
// Perform RUM error data ingestion
testLogger.info('Starting RUM error data ingestion');
await ingestRumErrors(page, 3); // Ingest 3 different error types
testLogger.info('RUM error data ingestion completed');
}
} catch (error) {
testLogger.error('Global setup failed', { error: error.message, stack: error.stack });
throw error;
} finally {
await context.close();
await browser.close();
testLogger.debug('Browser closed after global setup');
}
testLogger.info('Global setup completed successfully - ready for test execution');
}
/**
* Performs global test data ingestion
* @param {import('@playwright/test').Page} page
*/
async function performGlobalIngestion(page) {
const orgId = process.env["ORGNAME"];
const streamName = "e2e_automate";
const basicAuthCredentials = Buffer.from(
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
).toString('base64');
const headers = {
"Authorization": `Basic ${basicAuthCredentials}`,
"Content-Type": "application/json",
};
const baseUrl = (process.env.INGESTION_URL || process.env.ZO_BASE_URL || 'http://localhost:5080').replace(/\/+$/, '');
let response;
try {
response = await page.request.post(`${baseUrl}/api/${orgId}/${streamName}/_json`, {
headers: headers,
data: logsdata
});
} catch (requestError) {
testLogger.error('Data ingestion request failed', { error: requestError.message });
throw new Error(`Data ingestion request failed: ${requestError.message}`);
}
const responseStatus = response.status();
let responseData = null;
const contentType = response.headers()['content-type'] || '';
if (contentType.includes('application/json')) {
try {
const text = await response.text();
responseData = text ? JSON.parse(text) : null;
} catch (e) {
responseData = { error: 'Failed to parse JSON response', text: await response.text().catch(() => '') };
}
} else {
responseData = { text: await response.text().catch(() => '') };
}
testLogger.debug('Data ingestion API response received', {
status: responseStatus,
orgId,
streamName,
data: responseData
});
if (responseStatus !== 200) {
testLogger.error('Data ingestion failed', {
status: responseStatus,
response: responseData,
url: `${baseUrl}/api/${orgId}/${streamName}/_json`,
orgId,
streamName
});
throw new Error(`Data ingestion failed with status: ${responseStatus}. Response: ${JSON.stringify(responseData)}`);
}
testLogger.info('Global data ingestion successful', {
status: responseStatus,
orgId,
streamName,
responseData: responseData
});
}
/**
* Performs global metrics data ingestion
*/
async function performMetricsIngestion() {
try {
// Ingest test metrics data
const result = await metricsIngestion.ingestTestMetrics({
iterations: 20, // Send 20 batches of metrics for better test coverage
delay: 100, // 100ms between batches
useExternal: false // Use local OpenObserve instance
});
if (result.success) {
testLogger.info('Metrics ingestion successful', {
totalBatches: result.totalBatches,
successfulBatches: result.successfulBatches
});
} else {
testLogger.warn('Metrics ingestion completed with some failures', {
totalBatches: result.totalBatches,
successfulBatches: result.successfulBatches
});
}
// Also start continuous ingestion that will run during tests
// This runs in the background and doesn't block
metricsIngestion.ingestContinuously(120000, 10000).then(result => {
testLogger.debug('Background metrics ingestion completed', result);
}).catch(error => {
testLogger.error('Background metrics ingestion failed', { error: error.message });
});
} catch (error) {
testLogger.error('Metrics ingestion failed', { error: error.message });
// Don't fail the entire setup if metrics ingestion fails
// Tests can still run without metrics data
}
}
module.exports = globalSetup;