Files
openobserve/tests/ui-testing/pages/generalPages/editionFeaturesPage.js
T
3e89667b13 test: add edition-features documentation validation spec (#12573)
## Summary
- Adds a single GeneralTests spec (`edition-features.spec.js`) that
validates the header edition button, the `EnterpriseUpgradeDialog`,
every enterprise feature card, and the `o2.ws/*` documentation links
each card opens.
- Two tests live in the spec; exactly one runs per environment. The
active edition is detected from the rendered header button label (most
direct signal of what the user sees, tolerates frontend/backend
build-flag mismatches like an OSS frontend served by an ENT backend).
- Pairs with the same-named branch on `openobserve/o2-enterprise` so ENT
CI tree-merges this spec in and runs the ENT variant.

## What the spec validates
- Header button label per edition (`Get OpenObserve Enterprise (Free)`
vs `Edition: Enterprise`)
- Hero panel (title, badge, primary button, features title, Learn More
button)
- 20 enterprise feature cards (presence + external-link icon + BETA/HA
badges)
- Cloud-only `Bring Your Own Bucket` card stays hidden outside Cloud
- Every feature card click opens a popup that lands on the expected
`openobserve.ai` doc page
- Hero external link buttons (`Download Now`/`Learn More` on OSS, `Learn
More` on ENT) — `Get Free License` intentionally not clicked because it
switches the active org to the meta org

## Files
-
`tests/ui-testing/playwright-tests/GeneralTests/edition-features.spec.js`
(new)
- `tests/ui-testing/pages/generalPages/editionFeaturesPage.js` (new —
page object + truth table of slugs / expected doc paths)
- `tests/ui-testing/pages/page-manager.js` (register
`pm.editionFeaturesPage`)

## Test plan
- [x] OSS local run: 1 passed, 1 skipped (~2.0m)
- [x] ENT local run: 1 passed, 1 skipped (~2.0m)
- [x] Sentinel audit passed (POM compliance, no raw selectors in spec,
no hardcoded secrets, no console.log)
- [ ] OSS CI green on this PR
- [ ] ENT CI green on companion PR

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-11 01:14:55 +00:00

154 lines
9.0 KiB
JavaScript

// editionFeaturesPage.js - Page Object for the Enterprise Upgrade / Edition dialog
// Covers the header edition button (data-test="upgrade-to-enterprise-btn") and the
// EnterpriseUpgradeDialog feature list with o2.ws documentation short links.
import { expect } from '@playwright/test';
const testLogger = require('../../playwright-tests/utils/test-logger.js');
// Enterprise features rendered in the dialog for OSS and ENT builds (non-cloud layout).
// Source of truth: web/src/components/EnterpriseUpgradeDialog.vue (FEATURE_LINKS + enterpriseFeatures).
// expectedPath = fragment that must appear in the final openobserve.ai URL after the
// o2.ws redirect (resolved live on 2026-06-10).
// NOTE: workload_mgmt, query_opt, ai_assistant and telemetry_corr currently fall back
// to the /docs/ homepage — no dedicated short link is configured for them yet.
export const ENTERPRISE_FEATURES = [
{ name: 'Single Sign On', slug: 'sso', beta: false, ha: true, expectedPath: '/docs/user-guide/account-administration/identity-and-access-management/sso/' },
{ name: 'Role Based Access Control', slug: 'rbac', beta: false, ha: true, expectedPath: '/docs/user-guide/account-administration/identity-and-access-management/role-based-access-control/' },
{ name: 'Federated Search / Supercluster', slug: 'fed_search', beta: false, ha: true, expectedPath: '/docs/user-guide/data-exploration/federated-search/' },
{ name: 'Query Management', slug: 'query_mgmt', beta: false, ha: false, expectedPath: '/docs/user-guide/account-administration/management/query-management/' },
{ name: 'Workload Management', slug: 'workload_mgmt', beta: false, ha: false, expectedPath: '/docs/' },
{ name: 'Audit Trail', slug: 'audit_trail', beta: false, ha: false, expectedPath: '/docs/user-guide/account-administration/management/audit-trail/' },
{ name: 'Sensitive Data Redaction', slug: 'data_redact', beta: false, ha: false, expectedPath: '/docs/user-guide/account-administration/management/sensitive-data-redaction/' },
{ name: 'Pipeline Remote Destinations', slug: 'pipeline_remote', beta: false, ha: false, expectedPath: '/docs/user-guide/data-processing/pipelines/remote-destination/' },
{ name: 'Query Optimizer / Aggregation Cache', slug: 'query_opt', beta: false, ha: false, expectedPath: '/docs/' },
{ name: 'Incident Management', slug: 'incident_mgmt', beta: true, ha: true, expectedPath: '/docs/user-guide/analytics/incidents/' },
// sre_agent: o2.ws redirect resolves to /docs/administration/deployment/sre-agent-setup-guide/
// but that page issues a client-side redirect to /docs/enterprise-setup/sre-agent/ — assert
// the final user-visible URL.
{ name: 'SRE Agent', slug: 'sre_agent', beta: true, ha: true, expectedPath: '/docs/enterprise-setup/sre-agent/' },
{ name: 'O2 Assistant', slug: 'ai_assistant', beta: true, ha: true, expectedPath: '/docs/' },
{ name: 'Anomaly Detection', slug: 'anomaly_detect', beta: true, ha: false, expectedPath: '/docs/user-guide/analytics/alerts/anomaly-detection/' },
{ name: 'Metrics Auto Downsampling', slug: 'metrics_downsample', beta: false, ha: false, expectedPath: '/docs/user-guide/data-exploration/metrics/downsampling-metrics/' },
{ name: 'Log Patterns', slug: 'log_patterns', beta: false, ha: false, expectedPath: '/blog/log-patterns-automatic-pattern-extraction-faster-analysis/' },
{ name: 'MCP Server', slug: 'mcp_server', beta: false, ha: true, expectedPath: '/docs/integration/ai/mcp/' },
{ name: 'Rate Limiting', slug: 'rate_limit', beta: false, ha: false, expectedPath: '/docs/user-guide/account-administration/identity-and-access-management/quotas/how-quotas-work/' },
{ name: 'Broadcast Join', slug: 'broadcast_join', beta: false, ha: true, expectedPath: '/docs/user-guide/advanced/query-tuning/broadcast-join/' },
{ name: 'Logs, Metrics & Traces Correlation', slug: 'telemetry_corr', beta: false, ha: false, expectedPath: '/docs/' },
{ name: 'Service Graph', slug: 'service_maps', beta: false, ha: false, expectedPath: '/docs/user-guide/data-exploration/traces/service-graph/' },
];
// Cloud-only feature: rendered with v-show, so present in DOM but hidden on OSS/ENT.
export const CLOUD_ONLY_FEATURE_NAME = 'Bring Your Own Bucket';
// Hero panel link buttons (EnterpriseUpgradeDialog.vue openDownloadPage/openDocsLink).
export const HERO_LINKS = {
download_resources: { slug: 'download_resources', expectedPath: '/downloads/' },
// ent_install_guide: o2.ws redirect resolves to /docs/administration/deployment/openobserve-enterprise-edition-installation-guide/
// but that page issues a client-side redirect to /docs/enterprise-setup/ — assert the final user-visible URL.
ent_install_guide: { slug: 'ent_install_guide', expectedPath: '/docs/enterprise-setup/' },
// "lincense" typo is the real live URL on openobserve.ai
license_guide: { slug: 'license_guide', expectedPath: '/docs/user-guide/account-administration/management/lincense/' },
};
export class EditionFeaturesPage {
constructor(page) {
this.page = page;
// Header trigger button (same data-test in OSS and ENT, label differs)
this.editionButton = page.locator('[data-test="upgrade-to-enterprise-btn"]');
// Dialog
this.dialog = page.locator('[data-test="enterprise-upgrade-dialog"]');
this.heroTitle = this.dialog.locator('.hero-title');
this.offerBadge = this.dialog.locator('.offer-badge');
this.primaryButton = this.dialog.locator('.download-btn');
this.learnMoreButton = this.dialog.locator('.learn-more-btn');
this.featuresTitle = this.dialog.locator('.features-header h4');
this.featureItems = this.dialog.locator('.feature-list-item');
}
getFeatureItem(name) {
return this.featureItems.filter({
has: this.page.locator('.feature-name', { hasText: name }),
});
}
async expectEditionButtonLabel(expectedLabel) {
await expect(this.editionButton).toBeVisible({ timeout: 15000 });
await expect(this.editionButton).toContainText(expectedLabel);
}
// Detects the active edition from the rendered header button label. The dialog
// variant is driven by frontend build-time flags (VITE_OPENOBSERVE_ENTERPRISE /
// VITE_OPENOBSERVE_CLOUD via @/aws-exports), which can diverge from the backend
// binary's build_type (e.g. an OSS frontend bundle served by an enterprise
// backend). The button label is the most direct signal of what the user sees.
async detectEdition() {
const label = await this.editionButton.innerText({ timeout: 15000 });
const trimmed = label.trim();
if (trimmed.includes('Edition: Enterprise')) return 'enterprise';
if (trimmed.includes('Get OpenObserve Enterprise')) return 'opensource';
if (trimmed.includes('OpenObserve Features')) return 'cloud';
throw new Error(`Unrecognized edition button label: "${trimmed}" (raw: ${JSON.stringify(label)})`);
}
async openDialog() {
await this.editionButton.click();
await expect(this.dialog).toBeVisible({ timeout: 10000 });
}
async expectHero({ heroTitle, badgePattern, primaryButtonText, featuresTitle, hasLearnMore }) {
await expect(this.heroTitle).toHaveText(heroTitle);
await expect(this.offerBadge).toContainText(badgePattern);
await expect(this.primaryButton).toContainText(primaryButtonText);
await expect(this.featuresTitle).toHaveText(featuresTitle);
if (hasLearnMore) {
await expect(this.learnMoreButton).toContainText('Learn More');
}
}
async expectFeatureCard({ name, beta, ha }) {
const item = this.getFeatureItem(name);
await expect(item, `Feature card "${name}" should be visible`).toBeVisible();
await expect(item.locator('.external-link-icon'), `"${name}" should show external link icon`).toBeVisible();
if (beta) {
await expect(item.locator('.beta-badge'), `"${name}" should show BETA badge`).toBeVisible();
}
if (ha) {
await expect(item.locator('.ha-badge'), `"${name}" should show HA badge`).toBeVisible();
}
}
async expectCloudOnlyFeatureHidden() {
// byob is v-show gated: attached to the DOM but display:none on OSS/ENT
const item = this.getFeatureItem(CLOUD_ONLY_FEATURE_NAME);
await expect(item, `"${CLOUD_ONLY_FEATURE_NAME}" must stay hidden outside Cloud`).toBeHidden();
}
// Clicks a locator that triggers window.open and returns the popup's final URL
// after the o2.ws short link redirects to openobserve.ai.
async clickAndCapturePopupUrl(locator) {
const [popup] = await Promise.all([
this.page.context().waitForEvent('page', { timeout: 15000 }),
locator.click(),
]);
try {
await popup.waitForURL(/openobserve\.ai/, { timeout: 30000 });
// Wait for any in-page JS redirects to settle before sampling the URL
await popup.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
return popup.url();
} catch (e) {
testLogger.error('Popup never reached openobserve.ai — returning current URL', {
currentUrl: popup.url(),
error: e.message,
});
return popup.url();
} finally {
await popup.close().catch(() => {});
}
}
async captureFeatureLinkUrl(name) {
return this.clickAndCapturePopupUrl(this.getFeatureItem(name));
}
}