mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
801 lines
33 KiB
JavaScript
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;
|
|
}
|
|
}
|