mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +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`.
1086 lines
45 KiB
JavaScript
1086 lines
45 KiB
JavaScript
import { expect } from '@playwright/test';
|
|
|
|
const testLogger = require('../../playwright-tests/utils/test-logger.js');
|
|
|
|
/**
|
|
* Page Object for Correlation Settings page
|
|
* Route: /web/settings/correlation?org_identifier={orgId}
|
|
*
|
|
* Tabs:
|
|
* - Service Identity (identity)
|
|
* - Discovered Services (services)
|
|
* - Alert Correlation (alert-correlation)
|
|
*/
|
|
export class CorrelationSettingsPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// ==================== Main Page Selectors ====================
|
|
this.pageTitle = '.general-page-title';
|
|
this.pageSubtitle = '.general-page-subtitle';
|
|
|
|
// Tab selectors (OTabs - render as [role="tablist"] / [role="tab"])
|
|
// Note: tabs use name attribute: services, discovery, alert-correlation, field-aliases
|
|
this.tabsContainer = '[role="tablist"]';
|
|
// The Correlation Settings tabs wrapper (data-test container).
|
|
this.correlationSettingsTabs = '[data-test="correlation-settings-tabs"]';
|
|
// Save button on the Detection Rules ("Distinguish Services By") view.
|
|
this.saveConfigurationButtonName = 'Save Configuration';
|
|
// Placeholder text of the "+ Add field" OSelect trigger.
|
|
this.selectAFieldText = 'Select a field';
|
|
// Visible tab labels (post-revamp): the tab `name` attrs are
|
|
// services / discovery / alert-correlation / field-aliases, rendered as
|
|
// these i18n labels.
|
|
this.servicesTabName = 'Discovered Services';
|
|
this.serviceDiscoveryTabName = 'Detection Rules';
|
|
this.alertCorrelationTabName = 'Alert Grouping';
|
|
this.fieldAliasesTabName = 'Field Mappings';
|
|
// Detection Rules view (the "Distinguish Services By" configuration tab)
|
|
this.detectionRulesTabName = 'Detection Rules';
|
|
// "+ Add field" button that reveals the "Select a field" OSelect
|
|
this.addFieldButtonName = 'Add field';
|
|
|
|
// ==================== Service Identity Tab Selectors ====================
|
|
this.serviceIdentityBackwardBtn = '[data-test="correlation-service-identity-backward-btn"]';
|
|
this.serviceIdentityForwardBtn = '[data-test="correlation-service-identity-forward-btn"]';
|
|
this.serviceIdentitySearchInput = '[data-test="correlation-service-identity-available-search-input"]';
|
|
this.serviceIdentityResetBtn = '[data-test="correlation-service-identity-reset-btn"]';
|
|
this.serviceIdentitySaveBtn = '[data-test="correlation-service-identity-save-btn"]';
|
|
|
|
// ==================== Discovered Services Tab Selectors ====================
|
|
this.retryDiscoveredServicesBtn = '[data-test="retry-discovered-services-btn"]';
|
|
this.refreshDiscoveredServicesBtn = '[data-test="refresh-discovered-services-btn"]';
|
|
|
|
// ==================== Alert Correlation Tab Selectors ====================
|
|
this.dedupSettingsRefreshBtn = '[data-test="dedup-settings-refresh-btn"]';
|
|
this.organizationDedupEnableCheckbox = '[data-test="organization-deduplication-enable-checkbox"]';
|
|
this.crossAlertEnableCheckbox = '[data-test="organizationdeduplication-enable-cross-alert-checkbox"]';
|
|
this.defaultWindowInput = '[data-test="organizationdeduplication-default-window-input"]';
|
|
|
|
// ==================== Semantic Field Groups Selectors ====================
|
|
this.semanticGroupCategorySelect = '[data-test="semantic-group-category-select"]';
|
|
this.importJsonBtn = '[data-test="correlation-semanticfieldgroup-import-json-btn"]';
|
|
this.addCustomGroupBtn = '[data-test="correlation-semanticfieldgroup-add-custom-group-btn"]';
|
|
this.semanticFieldGroupSaveBtn = '[data-test="correlation-semanticfieldgroup-save-btn"]';
|
|
|
|
// ==================== Semantic Group Item Selectors ====================
|
|
this.semanticGroupDisplayInput = '[data-test="semantic-group-display-input"]';
|
|
// OInput forwards data-test to a non-interactive wrapper; the real <input>
|
|
// is the `-field` node and the inline validation message is `-error`.
|
|
this.semanticGroupDisplayInputField = '[data-test="semantic-group-display-input-field"]';
|
|
this.semanticGroupDisplayInputError = '[data-test="semantic-group-display-input-error"]';
|
|
this.semanticGroupScopeCheckbox = '[data-test="semantic-group-action-scope-chkbox"]';
|
|
this.semanticGroupStableCheckbox = '[data-test="semantic-group-action-stable-chkbox"]';
|
|
this.semanticGroupNormalizeCheckbox = '[data-test="semantic-group-action-normalize-chkbox"]';
|
|
this.semanticGroupRemoveBtn = '[data-test="semantic-group-remove-group-btn"]';
|
|
|
|
// ==================== Common Selectors ====================
|
|
this.loadingSpinner = '[data-test="discovered-services-loading-indicator"], [data-test="discovered-services-table-loading-indicator"]';
|
|
this.notification = '[role="alert"]';
|
|
}
|
|
|
|
// ==================== Test Data Setup ====================
|
|
|
|
/**
|
|
* Ensure semantic groups exist in the backend for testing fingerprint checkboxes
|
|
* Uses PUT /api/{org}/alerts/deduplication/semantic-groups to set semantic groups
|
|
* AND POST /api/{org}/alerts/deduplication/config to include them in the config
|
|
* The frontend reads semantic groups from the config endpoint first
|
|
* @param {string} orgId - Organization identifier
|
|
* @returns {Promise<boolean>} - True if successful
|
|
*/
|
|
async ensureSemanticGroupsExist(orgId) {
|
|
const baseUrl = process.env["ZO_BASE_URL"];
|
|
const username = process.env["ZO_ROOT_USER_EMAIL"];
|
|
const password = process.env["ZO_ROOT_USER_PASSWORD"];
|
|
const authHeader = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
|
|
|
|
// Define test semantic groups that cover common use cases.
|
|
// Post-revamp shape: {id, display, group, fields} only — the per-group
|
|
// flags (is_scope / is_stable / normalize) were removed.
|
|
const testSemanticGroups = [
|
|
{
|
|
id: 'k8s-cluster',
|
|
display: 'K8s Cluster',
|
|
group: 'Kubernetes',
|
|
fields: ['k8s.cluster.name', 'k8s_cluster_name'],
|
|
},
|
|
{
|
|
id: 'k8s-namespace',
|
|
display: 'K8s Namespace',
|
|
group: 'Kubernetes',
|
|
fields: ['k8s.namespace.name', 'k8s_namespace_name'],
|
|
},
|
|
{
|
|
id: 'k8s-deployment',
|
|
display: 'K8s Deployment',
|
|
group: 'Kubernetes',
|
|
fields: ['k8s.deployment.name', 'k8s_deployment_name'],
|
|
},
|
|
{
|
|
id: 'service',
|
|
display: 'Service',
|
|
group: 'Common',
|
|
fields: ['service.name', 'service_name', 'service'],
|
|
}
|
|
];
|
|
|
|
testLogger.info('Setting up test semantic groups via API', { orgId, groupCount: testSemanticGroups.length });
|
|
|
|
try {
|
|
// Step 1: PUT semantic groups to the semantic-groups endpoint
|
|
const putUrl = `${baseUrl}/api/${orgId}/alerts/deduplication/semantic-groups`;
|
|
testLogger.info('PUT semantic groups', { url: putUrl });
|
|
|
|
const putResponse = await fetch(putUrl, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': authHeader,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(testSemanticGroups)
|
|
});
|
|
|
|
const putText = await putResponse.text();
|
|
testLogger.info('PUT semantic-groups response', {
|
|
status: putResponse.status,
|
|
statusText: putResponse.statusText,
|
|
body: putText.substring(0, 500)
|
|
});
|
|
|
|
// Note: We intentionally do NOT POST to /config here.
|
|
// The config endpoint doesn't persist semantic_field_groups anyway (GET returns 0),
|
|
// and concurrent config POSTs in parallel tests cause race conditions where
|
|
// loadConfig() overwrites UI checkbox state with stale API values.
|
|
// The PUT above is the single source of truth for semantic groups.
|
|
|
|
return putResponse.ok;
|
|
} catch (error) {
|
|
testLogger.error('Failed to setup semantic groups', { error: error.message, stack: error.stack });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== Navigation ====================
|
|
|
|
async navigateToCorrelationSettings(orgId) {
|
|
const baseUrl = process.env.ZO_BASE_URL || 'http://localhost:5080';
|
|
await this.page.goto(`${baseUrl}/web/settings/correlation?org_identifier=${orgId}`);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
}
|
|
|
|
// ==================== Page Assertions ====================
|
|
|
|
async expectPageLoaded() {
|
|
// The tabs container is the reliable "page loaded" signal — the old
|
|
// `.general-page-title` class was removed in the revamp (the title is
|
|
// now [data-test="correlation-settings-page-title"]).
|
|
await expect(this.page.locator(this.correlationSettingsTabs)).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async expectAllTabsVisible() {
|
|
// Use getByRole for tabs or filter by text
|
|
const servicesTab = this.page.getByRole('tab', { name: this.servicesTabName });
|
|
const serviceDiscoveryTab = this.page.getByRole('tab', { name: this.serviceDiscoveryTabName });
|
|
const alertCorrelationTab = this.page.getByRole('tab', { name: this.alertCorrelationTabName });
|
|
const fieldAliasesTab = this.page.getByRole('tab', { name: this.fieldAliasesTabName });
|
|
|
|
await expect(servicesTab).toBeVisible({ timeout: 15000 });
|
|
await expect(serviceDiscoveryTab).toBeVisible();
|
|
await expect(alertCorrelationTab).toBeVisible();
|
|
await expect(fieldAliasesTab).toBeVisible();
|
|
}
|
|
|
|
// ==================== Tab Switching ====================
|
|
|
|
async clickServiceDiscoveryTab() {
|
|
const tab = this.page.getByRole('tab', { name: this.serviceDiscoveryTabName });
|
|
await tab.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async clickServicesTab() {
|
|
const tab = this.page.getByRole('tab', { name: this.servicesTabName });
|
|
await tab.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
// The "services" tab IS the Discovered Services tab (post-revamp label).
|
|
async clickDiscoveredServicesTab() {
|
|
await this.clickServicesTab();
|
|
}
|
|
|
|
async clickAlertCorrelationTab() {
|
|
const tab = this.page.getByRole('tab', { name: this.alertCorrelationTabName });
|
|
// Wait for semantic-groups API response that loadConfig() triggers on component mount.
|
|
// This prevents a race condition where the test interacts with checkboxes before
|
|
// loadConfig() completes and overwrites localConfig with API values.
|
|
const semanticGroupsPromise = this.page.waitForResponse(
|
|
resp => resp.url().includes('/alerts/deduplication/semantic-groups') && resp.status() === 200,
|
|
{ timeout: 15000 }
|
|
).catch(() => {
|
|
testLogger.info('Semantic groups API response not intercepted (may have already fired)');
|
|
});
|
|
await tab.click();
|
|
await semanticGroupsPromise;
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async expectServiceDiscoveryTabActive() {
|
|
const tab = this.page.getByRole('tab', { name: this.serviceDiscoveryTabName });
|
|
await expect(tab).toHaveAttribute('aria-selected', 'true', { timeout: 5000 });
|
|
}
|
|
|
|
async expectServicesTabActive() {
|
|
const tab = this.page.getByRole('tab', { name: this.servicesTabName });
|
|
await expect(tab).toHaveAttribute('aria-selected', 'true', { timeout: 5000 });
|
|
}
|
|
|
|
async expectAlertCorrelationTabActive() {
|
|
const tab = this.page.getByRole('tab', { name: this.alertCorrelationTabName });
|
|
await expect(tab).toHaveAttribute('aria-selected', 'true', { timeout: 5000 });
|
|
}
|
|
|
|
// ==================== Tab / Button Getters (raw-selector relocation) ====================
|
|
|
|
/**
|
|
* Field Mappings tab locator (a.k.a. Field Aliases).
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getFieldMappingsTab() {
|
|
return this.page.getByRole('tab', { name: this.fieldAliasesTabName });
|
|
}
|
|
|
|
/**
|
|
* Detection Rules tab locator (the "Distinguish Services By" configuration).
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getDetectionRulesTab() {
|
|
return this.page.getByRole('tab', { name: this.detectionRulesTabName });
|
|
}
|
|
|
|
/**
|
|
* "+ Add field" button on the Detection Rules view.
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getAddFieldButton() {
|
|
return this.page.getByRole('button', { name: this.addFieldButtonName }).first();
|
|
}
|
|
|
|
/**
|
|
* The Correlation Settings tabs container (data-test wrapper).
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getCorrelationSettingsTabs() {
|
|
return this.page.locator(this.correlationSettingsTabs);
|
|
}
|
|
|
|
/**
|
|
* "+ Add custom group" button on the Field Mappings view.
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getAddCustomGroupButton() {
|
|
return this.page.locator(this.addCustomGroupBtn);
|
|
}
|
|
|
|
/**
|
|
* Semantic-group display-input wrapper (data-test sits on the component
|
|
* wrapper; the editable element is inside).
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getSemanticGroupDisplayWrap() {
|
|
return this.page.locator(this.semanticGroupDisplayInput);
|
|
}
|
|
|
|
/**
|
|
* Save button for a semantic field group.
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getSemanticFieldGroupSaveButton() {
|
|
return this.page.locator(this.semanticFieldGroupSaveBtn);
|
|
}
|
|
|
|
/**
|
|
* "Save Configuration" button on the Detection Rules view.
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getSaveConfigurationButton() {
|
|
return this.page.getByRole('button', { name: this.saveConfigurationButtonName });
|
|
}
|
|
|
|
/**
|
|
* "Select a field" OSelect trigger (placeholder text, non-exact).
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getSelectAFieldTrigger() {
|
|
return this.page.getByText(this.selectAFieldText, { exact: false });
|
|
}
|
|
|
|
/**
|
|
* Role=option locator matched by name (string or RegExp).
|
|
* @param {string|RegExp} name - Option accessible name.
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getOptionByName(name) {
|
|
return this.page.getByRole('option', { name });
|
|
}
|
|
|
|
/**
|
|
* Visible text locator matched by string or RegExp.
|
|
* @param {string|RegExp} text - Text (or pattern) to match.
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getTextByPattern(text) {
|
|
return this.page.getByText(text);
|
|
}
|
|
|
|
async clickFieldAliasesTab() {
|
|
// Correlation Settings (Field Mappings / Field Aliases) is an ENTERPRISE-only
|
|
// feature — the tab only mounts on the enterprise build. Probe positively for
|
|
// the tab via its data-test (data-test-only policy) and return a boolean so
|
|
// callers can gate the suite to a clean SKIP on the OSS binary.
|
|
try {
|
|
const tab = this.page.locator('[data-test="correlation-settings-field-aliases-tab"]');
|
|
await tab.waitFor({ state: 'visible', timeout: 15000 });
|
|
await tab.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async expectFieldAliasesTabActive() {
|
|
const tab = this.page.getByRole('tab', { name: this.fieldAliasesTabName });
|
|
await expect(tab).toHaveAttribute('aria-selected', 'true', { timeout: 5000 });
|
|
}
|
|
|
|
// ==================== Tab Content Visibility Checks ====================
|
|
|
|
async expectServicesContentVisible() {
|
|
// Wait for the Services tab content to be visible
|
|
await expect(this.page.locator(this.refreshDiscoveredServicesBtn)).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async expectServiceDiscoveryContentVisible() {
|
|
// Detection Rules view: the "Save Configuration" button anchors it
|
|
// (the old dual-list Service Identity save-btn was removed in the revamp).
|
|
await expect(
|
|
this.page.getByRole('button', { name: this.saveConfigurationButtonName }).first(),
|
|
).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async expectFieldAliasesContentVisible() {
|
|
// Wait for the Field Aliases tab content to be visible
|
|
await expect(this.page.locator(this.importJsonBtn)).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
|
|
|
|
// ==================== Service Discovery (Configuration) Tab Actions ====================
|
|
|
|
async fillSearchAvailableDimensions(searchText) {
|
|
const searchInput = this.page.locator(this.serviceIdentitySearchInput);
|
|
await expect(searchInput).toBeVisible({ timeout: 10000 });
|
|
await searchInput.fill(searchText);
|
|
await this.page.waitForTimeout(500); // Wait for filtering
|
|
}
|
|
|
|
async clearSearchAvailableDimensions() {
|
|
const searchInput = this.page.locator(this.serviceIdentitySearchInput);
|
|
await searchInput.clear();
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
async clickAddDimensionButton() {
|
|
const btn = this.page.locator(this.serviceIdentityBackwardBtn);
|
|
// Wait for button to be enabled
|
|
await btn.waitFor({ state: 'visible', timeout: 5000 });
|
|
// Check if enabled, if not wait a bit
|
|
const isDisabled = await btn.getAttribute('disabled');
|
|
if (isDisabled !== null) {
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
await btn.click({ force: true });
|
|
}
|
|
|
|
async clickRemoveDimensionButton() {
|
|
const btn = this.page.locator(this.serviceIdentityForwardBtn);
|
|
// Wait for button to be visible
|
|
await btn.waitFor({ state: 'visible', timeout: 5000 });
|
|
// Check if enabled, if not wait a bit
|
|
const isDisabled = await btn.getAttribute('disabled');
|
|
if (isDisabled !== null) {
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
await btn.click({ force: true });
|
|
}
|
|
|
|
async clickResetButton() {
|
|
await this.page.locator(this.serviceIdentityResetBtn).click();
|
|
}
|
|
|
|
async clickSaveServiceIdentity() {
|
|
await this.page.locator(this.serviceIdentitySaveBtn).click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async expectAddDimensionButtonEnabled() {
|
|
const btn = this.page.locator(this.serviceIdentityBackwardBtn);
|
|
await expect(btn).not.toBeDisabled();
|
|
}
|
|
|
|
async expectAddDimensionButtonDisabled() {
|
|
const btn = this.page.locator(this.serviceIdentityBackwardBtn);
|
|
await expect(btn).toBeDisabled();
|
|
}
|
|
|
|
async expectSaveButtonVisible() {
|
|
await expect(this.page.locator(this.serviceIdentitySaveBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async expectResetButtonVisible() {
|
|
await expect(this.page.locator(this.serviceIdentityResetBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
// ==================== Discovered Services Tab Actions ====================
|
|
|
|
async expectDiscoveredServicesContentVisible() {
|
|
// Wait for loading to finish
|
|
await this.page.locator(this.loadingSpinner).waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
|
|
// Check for refresh button (always visible) or empty state
|
|
const refreshBtn = this.page.locator(this.refreshDiscoveredServicesBtn);
|
|
await expect(refreshBtn.first()).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async clickRefreshDiscoveredServices() {
|
|
await this.page.locator(this.refreshDiscoveredServicesBtn).first().click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
}
|
|
|
|
async expectDiscoveredServicesLoading() {
|
|
await expect(this.page.locator(this.loadingSpinner)).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectDiscoveredServicesLoaded() {
|
|
await this.page.locator(this.loadingSpinner).waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
|
|
}
|
|
|
|
async expectRefreshDiscoveredServicesButtonVisible() {
|
|
const refreshBtn = this.page.locator(this.refreshDiscoveredServicesBtn).first();
|
|
await expect(refreshBtn).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
// ==================== Alert Correlation Tab Actions ====================
|
|
|
|
async expectAlertCorrelationContentVisible() {
|
|
await expect(this.page.locator(this.organizationDedupEnableCheckbox)).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async clickEnableDeduplicationCheckbox() {
|
|
await this.page.locator(this.organizationDedupEnableCheckbox).click();
|
|
}
|
|
|
|
async clickEnableCrossAlertCheckbox() {
|
|
await this.page.locator(this.crossAlertEnableCheckbox).click();
|
|
}
|
|
|
|
async expectCrossAlertCheckboxVisible() {
|
|
await expect(this.page.locator(this.crossAlertEnableCheckbox)).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async expectCrossAlertCheckboxHidden() {
|
|
await expect(this.page.locator(this.crossAlertEnableCheckbox)).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Check if the cross-alert deduplication checkbox is currently checked
|
|
* @returns {Promise<boolean>} True if checkbox is checked
|
|
*/
|
|
async isCrossAlertCheckboxChecked() {
|
|
const checkbox = this.page.locator(this.crossAlertEnableCheckbox);
|
|
// The checkbox uses aria-checked attribute
|
|
const ariaChecked = await checkbox.getAttribute('aria-checked');
|
|
if (ariaChecked !== null) {
|
|
return ariaChecked === 'true';
|
|
}
|
|
// Fallback to checking the inner input if visible
|
|
const input = checkbox.locator('input[type="checkbox"]');
|
|
if (await input.count() > 0) {
|
|
return await input.isChecked();
|
|
}
|
|
// Last resort - check for checked class
|
|
const hasCheckedClass = await checkbox.evaluate(el => {
|
|
return el.classList.contains('checked') ||
|
|
el.closest('[role="checkbox"]')?.getAttribute('aria-checked') === 'true';
|
|
}).catch(() => false);
|
|
return hasCheckedClass;
|
|
}
|
|
|
|
async fillTimeWindowInput(minutes) {
|
|
// OFormInput forwards data-test to a wrapper <div>; the real control is
|
|
// the inner <input type="number">.
|
|
const wrap = this.page.locator(this.defaultWindowInput);
|
|
await expect(wrap).toBeVisible({ timeout: 5000 });
|
|
await wrap.locator('input').first().fill(String(minutes));
|
|
}
|
|
|
|
async getTimeWindowValue() {
|
|
return await this.page.locator(this.defaultWindowInput).locator('input').first().inputValue();
|
|
}
|
|
|
|
async clickRefreshDedupSettings() {
|
|
await this.page.locator(this.dedupSettingsRefreshBtn).click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async expectEnableDedupCheckboxVisible() {
|
|
await expect(this.page.locator(this.organizationDedupEnableCheckbox)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async expectTimeWindowInputVisible() {
|
|
await expect(this.page.locator(this.defaultWindowInput)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async expectDedupRefreshButtonVisible() {
|
|
await expect(this.page.locator(this.dedupSettingsRefreshBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
// ==================== Fingerprint Checkbox Actions ====================
|
|
|
|
async clickFingerprintCheckbox(groupId) {
|
|
const selector = `[data-test="organizationdeduplication-fingerprint-${groupId}-checkbox"]`;
|
|
await this.page.locator(selector).click();
|
|
}
|
|
|
|
async expectFingerprintCheckboxVisible(groupId) {
|
|
const selector = `[data-test="organizationdeduplication-fingerprint-${groupId}-checkbox"]`;
|
|
await expect(this.page.locator(selector)).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
// ==================== Semantic Field Groups Actions ====================
|
|
|
|
async clickCategorySelect() {
|
|
await this.page.locator(this.semanticGroupCategorySelect).click();
|
|
}
|
|
|
|
async selectCategory(categoryName) {
|
|
await this.clickCategorySelect();
|
|
await this.page.getByRole('option', { name: categoryName }).click();
|
|
}
|
|
|
|
async clickImportJsonButton() {
|
|
await this.page.locator(this.importJsonBtn).click();
|
|
}
|
|
|
|
async clickAddCustomGroupButton() {
|
|
await this.page.locator(this.addCustomGroupBtn).click();
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
// addGroup() unshifts the new group to the front of the list → use .first()
|
|
getDisplayNameInput() {
|
|
return this.page.locator(this.semanticGroupDisplayInputField).first();
|
|
}
|
|
|
|
getDisplayNameError() {
|
|
return this.page.locator(this.semanticGroupDisplayInputError).first();
|
|
}
|
|
|
|
async expectAddCustomGroupButtonVisible() {
|
|
await expect(this.page.locator(this.addCustomGroupBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async expectCategorySelectVisible() {
|
|
await expect(this.page.locator(this.semanticGroupCategorySelect)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async expectImportJsonButtonVisible() {
|
|
await expect(this.page.locator(this.importJsonBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
// ==================== Service Identity Navigation Buttons ====================
|
|
|
|
async expectBackwardButtonVisible() {
|
|
await expect(this.page.locator(this.serviceIdentityBackwardBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
async expectForwardButtonVisible() {
|
|
await expect(this.page.locator(this.serviceIdentityForwardBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
// ==================== Semantic Group Item Actions ====================
|
|
|
|
async fillSemanticGroupDisplayName(name, index = 0) {
|
|
const input = this.page.locator(this.semanticGroupDisplayInput).nth(index);
|
|
await input.fill(name);
|
|
}
|
|
|
|
async toggleSemanticGroupScope(index = 0) {
|
|
await this.page.locator(this.semanticGroupScopeCheckbox).nth(index).click();
|
|
}
|
|
|
|
async toggleSemanticGroupStable(index = 0) {
|
|
await this.page.locator(this.semanticGroupStableCheckbox).nth(index).click();
|
|
}
|
|
|
|
async toggleSemanticGroupNormalize(index = 0) {
|
|
await this.page.locator(this.semanticGroupNormalizeCheckbox).nth(index).click();
|
|
}
|
|
|
|
async clickRemoveSemanticGroup(index = 0) {
|
|
await this.page.locator(this.semanticGroupRemoveBtn).nth(index).click();
|
|
}
|
|
|
|
async getSemanticGroupCount() {
|
|
return await this.page.locator(this.semanticGroupDisplayInput).count();
|
|
}
|
|
|
|
// ==================== Notification Helpers ====================
|
|
|
|
async expectNotificationVisible(expectedText = null, timeout = 5000) {
|
|
const notification = this.page.locator(this.notification);
|
|
await expect(notification).toBeVisible({ timeout });
|
|
|
|
if (expectedText) {
|
|
const text = await notification.textContent();
|
|
expect(text.toLowerCase()).toContain(expectedText.toLowerCase());
|
|
}
|
|
}
|
|
|
|
async expectSuccessNotification(timeout = 5000) {
|
|
await this.expectNotificationVisible(null, timeout);
|
|
}
|
|
|
|
// ==================== Functional Test Helpers ====================
|
|
|
|
async getAvailableDimensionsCount() {
|
|
// Count items in the available dimensions list (right side)
|
|
const list = this.page.locator('[role="listbox"]').nth(1); // TODO: verify list container selector post-Reka migration
|
|
await list.waitFor({ state: 'visible', timeout: 10000 });
|
|
const items = list.locator('[role="option"], [data-test$="-item"]'); // TODO: verify data-test for dimension list items
|
|
return await items.count();
|
|
}
|
|
|
|
async expectSearchFiltersResults(searchTerm, expectedToFilter = true) {
|
|
// Get initial count
|
|
const initialCount = await this.getAvailableDimensionsCount();
|
|
|
|
// Enter search term
|
|
await this.fillSearchAvailableDimensions(searchTerm);
|
|
await this.page.waitForTimeout(500);
|
|
|
|
// Get filtered count
|
|
const filteredCount = await this.getAvailableDimensionsCount();
|
|
|
|
if (expectedToFilter) {
|
|
// Should have fewer or equal items after filtering
|
|
return filteredCount <= initialCount;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async expectPositiveNotification(timeout = 5000) {
|
|
const notification = this.page.locator('[role="alert"][class*="bg-positive"], [role="alert"].bg-positive');
|
|
await expect(notification).toBeVisible({ timeout });
|
|
}
|
|
|
|
async waitForNotificationToDisappear(timeout = 10000) {
|
|
await this.page.locator(this.notification).waitFor({ state: 'hidden', timeout }).catch(() => {});
|
|
}
|
|
|
|
async refreshPage() {
|
|
await this.page.reload();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
}
|
|
|
|
async isDedupCheckboxChecked() {
|
|
const checkbox = this.page.locator(this.organizationDedupEnableCheckbox);
|
|
// Checkboxes use aria-checked or have a specific class when checked
|
|
const isChecked = await checkbox.getAttribute('aria-checked');
|
|
if (isChecked !== null) {
|
|
return isChecked === 'true';
|
|
}
|
|
// Fallback: check for checked class
|
|
const classList = await checkbox.getAttribute('class');
|
|
return classList?.includes('checked');
|
|
}
|
|
|
|
// ==================== Alert Correlation Save Button ====================
|
|
|
|
async clickSaveAlertCorrelation() {
|
|
// The save button in Alert Correlation tab
|
|
const saveBtn = this.page.getByRole('button', { name: /save/i }).last();
|
|
await saveBtn.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
async expectAlertCorrelationSaveSuccess() {
|
|
// Wait for positive notification about saved settings
|
|
const notification = this.page.locator('[role="alert"]');
|
|
await expect(notification).toBeVisible({ timeout: 10000 });
|
|
const text = await notification.textContent();
|
|
return text?.toLowerCase().includes('saved') || text?.toLowerCase().includes('success');
|
|
}
|
|
|
|
// ==================== Phase 1 High Priority - Dimension Management ====================
|
|
|
|
/**
|
|
* Select an available dimension from the right list by name
|
|
* @param {string} dimensionName - Name of dimension to select
|
|
*/
|
|
async selectAvailableDimension(dimensionName) {
|
|
// Available dimensions are in the second list (right side) within service-identity-config
|
|
const serviceConfig = this.page.locator('.service-identity-config');
|
|
await serviceConfig.waitFor({ state: 'visible', timeout: 10000 });
|
|
// Bordered lists (from the bordered prop) with the h-80 height class
|
|
const lists = serviceConfig.locator('[role="listbox"]');
|
|
const rightList = lists.last();
|
|
await rightList.waitFor({ state: 'visible', timeout: 5000 });
|
|
const item = rightList.locator('[role="option"], [data-test$="-item"]').filter({ hasText: dimensionName }).first(); // TODO: verify data-test
|
|
await item.click();
|
|
// Wait for button to become enabled
|
|
await this.page.waitForTimeout(300);
|
|
}
|
|
|
|
/**
|
|
* Select a priority dimension from the left list by name
|
|
* @param {string} dimensionName - Name of dimension to select
|
|
*/
|
|
async selectPriorityDimension(dimensionName) {
|
|
// Priority dimensions are in the first list (left side) within service-identity-config
|
|
const serviceConfig = this.page.locator('.service-identity-config');
|
|
await serviceConfig.waitFor({ state: 'visible', timeout: 10000 });
|
|
// Bordered lists (from the bordered prop) with the h-80 height class
|
|
const lists = serviceConfig.locator('[role="listbox"]');
|
|
const leftList = lists.first();
|
|
await leftList.waitFor({ state: 'visible', timeout: 5000 });
|
|
const item = leftList.locator('[role="option"], [data-test$="-item"]').filter({ hasText: dimensionName }).first(); // TODO: verify data-test
|
|
await item.click();
|
|
// Wait for button to become enabled
|
|
await this.page.waitForTimeout(300);
|
|
}
|
|
|
|
/**
|
|
* Get count of dimensions in the priority list (left side)
|
|
* @returns {Promise<number>}
|
|
*/
|
|
async getPriorityDimensionsCount() {
|
|
const serviceConfig = this.page.locator('.service-identity-config');
|
|
await serviceConfig.waitFor({ state: 'visible', timeout: 10000 });
|
|
// Bordered lists (from the bordered prop) with the h-80 height class
|
|
const lists = serviceConfig.locator('[role="listbox"]');
|
|
const leftList = lists.first();
|
|
await leftList.waitFor({ state: 'visible', timeout: 5000 });
|
|
// Exclude empty state items (the "No dimensions configured" message)
|
|
const items = leftList.locator('[role="option"]:not(:has-text("No")), [data-test$="-item"]:not(:has-text("No"))'); // TODO: verify data-test
|
|
return await items.count();
|
|
}
|
|
|
|
/**
|
|
* Check if a dimension exists in the priority list
|
|
* @param {string} dimensionName - Name of dimension to check
|
|
*/
|
|
async expectPriorityDimensionExists(dimensionName) {
|
|
const serviceConfig = this.page.locator('.service-identity-config');
|
|
// Bordered lists (from the bordered prop) with the h-80 height class
|
|
const lists = serviceConfig.locator('[role="listbox"]');
|
|
const leftList = lists.first();
|
|
const item = leftList.locator('[role="option"], [data-test$="-item"]').filter({ hasText: dimensionName }).first(); // TODO: verify data-test
|
|
await expect(item).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Check if a dimension does NOT exist in the priority list
|
|
* @param {string} dimensionName - Name of dimension to check
|
|
*/
|
|
async expectPriorityDimensionNotExists(dimensionName) {
|
|
const serviceConfig = this.page.locator('.service-identity-config');
|
|
// Bordered lists (from the bordered prop) with the h-80 height class
|
|
const lists = serviceConfig.locator('[role="listbox"]');
|
|
const leftList = lists.first();
|
|
const item = leftList.locator('[role="option"], [data-test$="-item"]').filter({ hasText: dimensionName }).first(); // TODO: verify data-test
|
|
await expect(item).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
// ==================== Phase 1 High Priority - Fingerprint Groups ====================
|
|
|
|
/**
|
|
* Get all fingerprint group checkboxes
|
|
* @returns {import('@playwright/test').Locator}
|
|
*/
|
|
getFingerprintGroupCheckboxes() {
|
|
return this.page.locator('[data-test^="organizationdeduplication-fingerprint-"]');
|
|
}
|
|
|
|
/**
|
|
* Expect fingerprint groups section to be visible
|
|
*/
|
|
async expectFingerprintGroupsVisible() {
|
|
const checkboxes = this.getFingerprintGroupCheckboxes();
|
|
await expect(checkboxes.first()).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Expect fingerprint groups section to be hidden
|
|
*/
|
|
async expectFingerprintGroupsHidden() {
|
|
const checkboxes = this.page.locator('[data-test^="organizationdeduplication-fingerprint-"]');
|
|
await expect(checkboxes.first()).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Get count of fingerprint group checkboxes
|
|
* @returns {Promise<number>}
|
|
*/
|
|
async getFingerprintGroupsCount() {
|
|
const checkboxes = this.getFingerprintGroupCheckboxes();
|
|
return await checkboxes.count();
|
|
}
|
|
|
|
/**
|
|
* Check if a specific fingerprint group is checked
|
|
* @param {string} groupId - The group ID
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async isFingerprintGroupChecked(groupId) {
|
|
const selector = `[data-test="organizationdeduplication-fingerprint-${groupId}-checkbox"]`;
|
|
const checkbox = this.page.locator(selector);
|
|
const isChecked = await checkbox.getAttribute('aria-checked');
|
|
if (isChecked !== null) {
|
|
return isChecked === 'true';
|
|
}
|
|
const classList = await checkbox.getAttribute('class');
|
|
return classList?.includes('checked');
|
|
}
|
|
|
|
/**
|
|
* Expect validation error notification to be visible
|
|
* @param {string} expectedMessage - Expected error message (partial match)
|
|
*/
|
|
async expectValidationErrorVisible(expectedMessage = null) {
|
|
const notification = this.page.locator('[role="alert"][class*="bg-negative"], [role="alert"].bg-negative, [role="alert"]');
|
|
await expect(notification.first()).toBeVisible({ timeout: 10000 });
|
|
if (expectedMessage) {
|
|
const text = await notification.first().textContent();
|
|
expect(text.toLowerCase()).toContain(expectedMessage.toLowerCase());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Click the Cancel button in Alert Correlation tab
|
|
*/
|
|
async clickCancelAlertCorrelation() {
|
|
const cancelBtn = this.page.getByRole('button', { name: /cancel/i });
|
|
await cancelBtn.click();
|
|
}
|
|
|
|
// ==================== Phase 1 High Priority - Discovered Services ====================
|
|
|
|
/**
|
|
* Expect loading spinner to be visible (during data fetch)
|
|
*/
|
|
async expectLoadingSpinnerVisible() {
|
|
await expect(this.page.locator(this.loadingSpinner)).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
/**
|
|
* Expect discovered services data to be loaded (content visible)
|
|
*/
|
|
async expectServicesDataLoaded() {
|
|
await this.page.locator(this.loadingSpinner).waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
|
|
// Check for either services content or empty state
|
|
const hasContent = await this.page.locator('table, [role="listbox"], .discovered-services-content').first().isVisible().catch(() => false);
|
|
const hasEmptyState = await this.page.locator('text=/no.*service|empty/i').first().isVisible().catch(() => false);
|
|
return hasContent || hasEmptyState;
|
|
}
|
|
|
|
/**
|
|
* Expect empty state message when no services discovered
|
|
*/
|
|
async expectEmptyStateVisible() {
|
|
const emptyState = this.page.locator('text=/no.*service|empty|no data/i').first();
|
|
await expect(emptyState).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Click the Retry button when in error state
|
|
*/
|
|
async clickRetryButton() {
|
|
await this.page.locator(this.retryDiscoveredServicesBtn).click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
}
|
|
|
|
/**
|
|
* Expect retry button to be visible (error state)
|
|
*/
|
|
async expectRetryButtonVisible() {
|
|
await expect(this.page.locator(this.retryDiscoveredServicesBtn)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
// ==================== Phase 1 High Priority - Error/Success Notifications ====================
|
|
|
|
/**
|
|
* Expect error notification to be visible
|
|
* @param {string} message - Expected message content (optional)
|
|
*/
|
|
async expectErrorNotificationVisible(message = null) {
|
|
const notification = this.page.locator('[role="alert"][class*="bg-negative"], [role="alert"].bg-negative');
|
|
await expect(notification.first()).toBeVisible({ timeout: 10000 });
|
|
if (message) {
|
|
const text = await notification.first().textContent();
|
|
expect(text.toLowerCase()).toContain(message.toLowerCase());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expect success notification to be visible
|
|
* @param {string} message - Expected message content (optional)
|
|
*/
|
|
async expectSuccessNotificationVisible(message = null) {
|
|
const notification = this.page.locator('[role="alert"][class*="bg-positive"], [role="alert"].bg-positive, [role="alert"]');
|
|
await expect(notification.first()).toBeVisible({ timeout: 10000 });
|
|
if (message) {
|
|
const text = await notification.first().textContent();
|
|
expect(text.toLowerCase()).toContain(message.toLowerCase());
|
|
}
|
|
}
|
|
|
|
// ==================== Phase 1 High Priority - Reset Functionality ====================
|
|
|
|
/**
|
|
* Click Reset to Defaults button and wait for confirmation
|
|
*/
|
|
async clickResetToDefaults() {
|
|
await this.page.locator(this.serviceIdentityResetBtn).click();
|
|
// Wait for any confirmation dialog or immediate reset
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
/**
|
|
* Confirm reset dialog if present
|
|
*/
|
|
async confirmResetDialog() {
|
|
const confirmBtn = this.page.getByRole('button', { name: /confirm|yes|ok/i });
|
|
if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await confirmBtn.click();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the first available dimension name for testing
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async getFirstAvailableDimensionName() {
|
|
const serviceConfig = this.page.locator('.service-identity-config');
|
|
await serviceConfig.waitFor({ state: 'visible', timeout: 10000 });
|
|
// Bordered lists (from the bordered prop) with the h-80 height class
|
|
const lists = serviceConfig.locator('[role="listbox"]');
|
|
const rightList = lists.last();
|
|
await rightList.waitFor({ state: 'visible', timeout: 5000 });
|
|
const firstItem = rightList.locator('[role="option"]:not(:has-text("No")), [data-test$="-item"]:not(:has-text("No"))').first(); // TODO: verify data-test
|
|
// Try to get just the label text
|
|
const label = firstItem.getByText(/.+/).first();
|
|
if (await label.isVisible().catch(() => false)) {
|
|
return await label.textContent();
|
|
}
|
|
// Fallback to full text
|
|
const fullText = await firstItem.textContent();
|
|
// Extract dimension name (usually formatted like "Name (id)")
|
|
const match = fullText.match(/([A-Za-z0-9\s\-_]+\s*\([^)]+\))/);
|
|
return match ? match[1].trim() : fullText.trim();
|
|
}
|
|
|
|
/**
|
|
* Get the first priority dimension name for testing
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async getFirstPriorityDimensionName() {
|
|
const serviceConfig = this.page.locator('.service-identity-config');
|
|
await serviceConfig.waitFor({ state: 'visible', timeout: 10000 });
|
|
// Bordered lists (from the bordered prop) with the h-80 height class
|
|
const lists = serviceConfig.locator('[role="listbox"]');
|
|
const leftList = lists.first();
|
|
await leftList.waitFor({ state: 'visible', timeout: 5000 });
|
|
const firstItem = leftList.locator('[role="option"]:not(:has-text("No")), [data-test$="-item"]:not(:has-text("No"))').first(); // TODO: verify data-test
|
|
// Try to get just the label text
|
|
const label = firstItem.getByText(/.+/).first();
|
|
if (await label.isVisible().catch(() => false)) {
|
|
return await label.textContent();
|
|
}
|
|
// Fallback to full text
|
|
const fullText = await firstItem.textContent();
|
|
// Extract dimension name
|
|
const match = fullText.match(/([A-Za-z0-9\s\-_]+\s*\([^)]+\))/);
|
|
return match ? match[1].trim() : fullText.trim();
|
|
}
|
|
|
|
/**
|
|
* Check if backward (add) button is enabled
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async isAddButtonEnabled() {
|
|
try {
|
|
const btn = this.page.locator(this.serviceIdentityBackwardBtn);
|
|
await btn.waitFor({ state: 'visible', timeout: 5000 });
|
|
const isDisabled = await btn.getAttribute('disabled', { timeout: 2000 });
|
|
return isDisabled === null;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if forward (remove) button is enabled
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async isRemoveButtonEnabled() {
|
|
try {
|
|
const btn = this.page.locator(this.serviceIdentityForwardBtn);
|
|
await btn.waitFor({ state: 'visible', timeout: 5000 });
|
|
const isDisabled = await btn.getAttribute('disabled', { timeout: 2000 });
|
|
return isDisabled === null;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== Utility Helpers ====================
|
|
|
|
/**
|
|
* Wait for UI to stabilize after an action (e.g., selection, toggle)
|
|
* @param {number} ms - Milliseconds to wait (default: 500)
|
|
*/
|
|
async waitForUIStabilization(ms = 500) {
|
|
await this.page.waitForTimeout(ms);
|
|
}
|
|
|
|
/**
|
|
* Short wait for quick UI updates (e.g., button state changes)
|
|
* @param {number} ms - Milliseconds to wait (default: 300)
|
|
*/
|
|
async waitForQuickUpdate(ms = 300) {
|
|
await this.page.waitForTimeout(ms);
|
|
}
|
|
|
|
/**
|
|
* Opens the TimeRangeEditor dialog from the Correlation Settings page.
|
|
* Tries the known trigger button selectors for the time range editor.
|
|
* Returns true if the dialog was opened successfully, false otherwise.
|
|
*/
|
|
async openTimeRangeEditor() {
|
|
const trigger = this.page.locator(
|
|
'[data-test="correlation-time-range-edit-btn"], [data-test="time-range-editor-trigger"]'
|
|
);
|
|
const triggerVisible = await trigger.isVisible({ timeout: 5000 }).catch(() => false);
|
|
if (!triggerVisible) {
|
|
testLogger.warn('TimeRangeEditor trigger not found — component may not be available in this environment');
|
|
return false;
|
|
}
|
|
await trigger.click();
|
|
const dialogVisible = await this.page.locator('[data-test="time-range-editor-dialog"]')
|
|
.isVisible({ timeout: 5000 }).catch(() => false);
|
|
return dialogVisible;
|
|
}
|
|
}
|