Files
openobserve/tests/ui-testing/pages/generalPages/correlationSettingsPage.js
T
Hengfei Yangandgithub-actions[bot] 9298096c19 feat(alerts): suppress repeat notifications per state change (#16071)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-30 23:41:21 +08:00

801 lines
33 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:
* - Discovered Services (services)
* - Detection Rules (discovery)
* - Field Mappings (field-aliases)
*/
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, 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 / field-aliases, rendered as
// these i18n labels.
this.servicesTabName = 'Discovered Services';
this.serviceDiscoveryTabName = 'Detection Rules';
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"]';
// ==================== 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"]';
}
// ==================== 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 fieldAliasesTab = this.page.getByRole('tab', { name: this.fieldAliasesTabName });
await expect(servicesTab).toBeVisible({ timeout: 15000 });
await expect(serviceDiscoveryTab).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 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 });
}
// ==================== 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 });
}
// ==================== 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(() => {});
}
// ==================== 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 - Validation Notifications ====================
/**
* 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());
}
}
// ==================== 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;
}
}