mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
945 lines
49 KiB
JavaScript
945 lines
49 KiB
JavaScript
const logData = require("../../fixtures/log.json");
|
|
const { expect } = require('@playwright/test');
|
|
const testLogger = require('../../playwright-tests/utils/test-logger.js');
|
|
|
|
class SchemaPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// =========================================================
|
|
// Static class-member locators (POM strict — §3)
|
|
// =========================================================
|
|
// Navigation / menu links
|
|
this.menuStreamItem = page.locator('[data-test="menu-link-\\/streams-item"]');
|
|
this.menuHomeItem = page.locator('[data-test="menu-link-\\/-item"]');
|
|
this.menuLogsItem = page.locator('[data-test="menu-link-\\/logs-item"]');
|
|
|
|
// Streams listing page (LogStream.vue)
|
|
// OInput wrapper `streams-search-stream-input` → inner native input is auto-derived `-field`
|
|
this.streamsSearchStreamInput = page.locator('[data-test="streams-search-stream-input"]');
|
|
this.streamsSearchStreamInputField = page.locator('[data-test="streams-search-stream-input-field"]');
|
|
this.logStreamRefreshStatsBtn = page.locator('[data-test="log-stream-refresh-stats-btn"]');
|
|
this.logStreamAddStreamBtn = page.locator('[data-test="log-stream-add-stream-btn"]');
|
|
|
|
// AddStream drawer (AddStream.vue migrated to ODrawer)
|
|
this.addStreamDialog = page.locator('[data-test="add-stream-dialog"]');
|
|
this.addStreamNameInputField = page.locator('[data-test="add-stream-dialog"] [data-test="add-stream-name-input-field"]');
|
|
this.addStreamTypeInput = page.locator('[data-test="add-stream-dialog"] [data-test="add-stream-type-input"]');
|
|
this.addStreamTypeOptionLogs = page.locator('[data-test="add-stream-type-input-option"][data-test-value="logs"]');
|
|
// AddStream renders ODrawer (isInPipeline=false) on the Streams page — save via ODrawer primary button.
|
|
this.addStreamSaveBtn = page.locator('[data-test="add-stream-dialog"] [data-test="o-dialog-primary-btn"]');
|
|
|
|
// Schema drawer (schema.vue migrated to ODrawer with data-test="schema-drawer")
|
|
this.schemaDrawer = page.locator('[data-test="schema-drawer"]');
|
|
this.schemaDrawerCloseBtn = page.locator('[data-test="schema-drawer"] [data-test="o-drawer-close-btn"]');
|
|
this.schemaTitleText = page.locator('[data-test="schema-title-text"]');
|
|
this.schemaAddFieldButton = page.locator('[data-test="schema-add-field-button"]');
|
|
this.schemaUpdateSettingsButton = page.locator('[data-test="schema-update-settings-button"]');
|
|
this.schemaCancelButton = page.locator('[data-test="schema-cancel-button"]');
|
|
this.schemaAddFieldsTitle = page.locator('[data-test="schema-add-fields-title"]');
|
|
this.schemaFieldSearchInput = page.locator('[data-test="schema-field-search-input"]');
|
|
this.schemaFieldSearchInputField = page.locator('[data-test="schema-field-search-input-field"]');
|
|
this.schemaDeleteButton = page.locator('[data-test="schema-delete-button"]');
|
|
this.schemaLogStreamFieldMappingTable = page.locator('[data-test="schema-log-stream-field-mapping-table"]');
|
|
// Schema fields tabs (OToggleGroup wrapper)
|
|
this.schemaFieldsTabs = page.locator('[data-test="schema-fields-tabs"]');
|
|
// Confirm dialog (ConfirmDialog migrated to ODialog)
|
|
this.confirmDialogPrimaryBtn = page.locator('[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]');
|
|
// Generic ODialog primary button — used when no confirm-dialog wrapper is present.
|
|
this.dialogPrimaryBtn = page.locator('[data-test="o-dialog-primary-btn"]');
|
|
|
|
// Stream delete dialog (LogStream.vue → ODialog)
|
|
this.logStreamDeleteDialog = page.locator('[data-test="log-stream-delete-dialog"]');
|
|
this.logStreamDeleteDialogOk = page.locator('[data-test="log-stream-delete-dialog"] [data-test="o-dialog-primary-btn"]');
|
|
|
|
// Logs page query / search bar
|
|
this.logsSearchBarQueryEditor = page.locator('[data-test="logs-search-bar-query-editor"]');
|
|
this.logsSearchBarRefreshBtn = page.locator('[data-test="logs-search-bar-refresh-btn"]');
|
|
this.logsSearchErrorMessage = page.locator('[data-test="logs-search-error-state"]');
|
|
this.dateTimeBtn = page.locator('[data-test="date-time-btn"]');
|
|
this.dateTimeRelativeTab = page.locator('[data-test="date-time-relative-tab"]');
|
|
this.logTableExpandMenu = page.locator('[data-test="o2-table-expand-1"]');
|
|
|
|
// IndexList / stream picker (OSelect in listbox/multiple mode)
|
|
// OSelect uses `inheritAttrs: false` and forwards `data-test` onto the
|
|
// root wrapper <div>, while the actual clickable PopoverTrigger button
|
|
// is published as `${parent}-trigger`. Clicking the wrapper div does
|
|
// NOT open the popover — we must click the `-trigger` button.
|
|
this.logSearchIndexSelectStream = page.locator('[data-test="log-search-index-list-select-stream-trigger"]');
|
|
// OSelect popover renders with `${parent}-popover`; the inner ListboxFilter
|
|
// is data-tested via `${parent}-search` (forwarded from OSelect.vue).
|
|
this.logSearchStreamPopover = page.locator('[data-test="log-search-index-list-select-stream-popover"]');
|
|
this.logSearchStreamPopoverSearch = page.locator('[data-test="log-search-index-list-select-stream-search"]');
|
|
this.logSearchIndexFieldSearchInput = page.locator('[data-test="log-search-index-list-field-search-input"]');
|
|
this.logSearchIndexFieldsTable = page.locator('[data-test="log-search-index-list-fields-table"]');
|
|
this.logsUserDefinedFieldsBtn = page.locator('[data-test="logs-user-defined-fields-btn"]');
|
|
this.logsAllFieldsBtn = page.locator('[data-test="logs-all-fields-btn"]');
|
|
this.logSearchIndexAllFieldBtn = page.locator('[data-test="log-search-index-list-interesting-_all-field-btn"]');
|
|
this.logExpandDetailAllKey = page.locator('[data-test="log-expand-detail-key-_all"]');
|
|
|
|
// VRL / show-query toggle
|
|
this.fnEditor = page.locator('[data-test="logs-vrl-function-editor"]');
|
|
this.showQueryToggleBtn = page.locator('[data-test="logs-search-bar-show-query-toggle-btn"]');
|
|
// Utilities dropdown — the toggle is now inside an ODropdown in SearchBar.vue
|
|
this.utilitiesMenuBtn = page.locator('[data-test="logs-search-bar-utilities-menu-btn"]');
|
|
this.menuTransformEditorToggleBtn = page.locator('[data-test="logs-search-bar-menu-transform-editor-toggle-btn"]');
|
|
|
|
// Per-row action buttons on the LogStream OTable (cell-actions template)
|
|
this.logStreamSchemaBtnFirst = page.locator('[data-test="log-stream-schema-btn"]').first();
|
|
this.logStreamNameCells = page.locator('[data-test^="log-stream-name-cell-"]');
|
|
this.logStreamExploreBtnFirst = page.locator('[data-test="log-stream-explore-btn"]').first();
|
|
|
|
// Field row expanders on the log search index list (FieldRow templates)
|
|
this.logSearchExpandAllField = page.locator('[data-test="log-search-expand-_all-field"]');
|
|
this.logSearchExpandTimestampField = page.locator('[data-test="log-search-expand-_timestamp-field"]');
|
|
|
|
// Stream-fields-table expanders scoped under logSearchIndexFieldsTable
|
|
this.logSearchIndexFieldsTableAllField = page.locator('[data-test="log-search-index-list-fields-table"] [data-test="log-search-expand-_all-field"]');
|
|
this.logSearchIndexFieldsTableTimestampField = page.locator('[data-test="log-search-index-list-fields-table"] [data-test="log-search-expand-_timestamp-field"]');
|
|
|
|
// Stream picker option factory selector — built per call via getStreamOption.
|
|
this.logsSearchResultLoading = page.locator('[data-test="logs-search-result-loading"]');
|
|
|
|
// AddStream / Schema field add row inputs — schema.vue StreamFieldsInputs row
|
|
this.schemaNewFieldNameInputField = page.locator('[data-test="schema-new-field-name-input-field"], [data-test="add-stream-field-name-input-field"]').first();
|
|
this.schemaNewFieldDataTypeSelect = page.locator('[data-test="schema-new-field-data-type-select"], [data-test="add-stream-field-data-type-select"]').first();
|
|
|
|
// Counter locator used for diagnostic schema-element counting
|
|
this.schemaElementCountLocator = page.locator('[data-test*="schema"]');
|
|
|
|
// =========================================================
|
|
// Timeline chip locators (schema.vue #header-right slot)
|
|
// NEEDS SELECTOR — no data-test attributes exist on timeline
|
|
// chip elements yet. Fallback locators scoped to the schema
|
|
// drawer use structural relationships (img + span presence).
|
|
// =========================================================
|
|
// Chip wrapper: the div inside schema-drawer that contains
|
|
// an img (timeline icon) and is NOT the close-button area.
|
|
this.schemaTimelineChip = page.locator('[data-test="schema-drawer"] div').filter({
|
|
has: page.locator('img'),
|
|
}).filter({
|
|
hasNot: page.locator('[data-test="o-drawer-close-btn"]'),
|
|
}).first();
|
|
|
|
// Timezone label span (e.g. "UTC", "America/New_York")
|
|
// The first <span> inside the chip is the timezone label.
|
|
this.schemaTimelineTimezoneLabel = this.schemaTimelineChip.locator('span').first();
|
|
|
|
// The div containing the doc_time range text (innermost text div with
|
|
// class text-text-body — avoids strict mode violation from nested divs
|
|
// that also contain the → character via descendant text).
|
|
this.schemaTimelineDocTimeRange = this.schemaTimelineChip.locator('div.text-text-body');
|
|
|
|
// Tab selectors within the schema drawer
|
|
this.schemaSettingsTab = page.locator('[data-test="schema-settings-tab"]');
|
|
this.schemaConfigurationTab = page.locator('[data-test="schema-configuration-tab"]');
|
|
|
|
// =========================================================
|
|
// Legacy schemaLocators map preserved for any callers that
|
|
// still reference it via PageManager — values stay aligned
|
|
// with the hoisted locators above so behaviour is identical.
|
|
// =========================================================
|
|
this.schemaLocators = {
|
|
menuStreamItem: '[data-test="menu-link-\\/streams-item"]',
|
|
streamSearchPlaceholder: 'Search Stream',
|
|
streamDetailButton: 'Stream Detail',
|
|
streamDeleteKubernetesCheckbox: '[data-test="schema-stream-delete-kubernetes_annotations_kubectl_kubernetes_io_default_container-field-fts-key-checkbox"]',
|
|
streamDeleteKubernetesPspCheckbox: '[data-test="schema-stream-delete-kubernetes_annotations_kubernetes_io_psp-field-fts-key-checkbox"]',
|
|
schemaAddFieldButton: '[data-test="schema-add-field-button"]',
|
|
schemaUpdateSettingsButton: '[data-test="schema-update-settings-button"]',
|
|
tabSchemaFields: '[data-test="tab-schemaFields"]',
|
|
tabAllFields: '[data-test="tab-allFields"]',
|
|
explorerButton: 'Explore',
|
|
closeButton: 'close',
|
|
dateTimeBtn: '[data-test="date-time-btn"]',
|
|
dateTimeRelativeTab: '[data-test="date-time-relative-tab"]',
|
|
logTableExpandMenu: '[data-test="o2-table-expand-1"]',
|
|
logsUserDefinedFieldsBtn: '[data-test="logs-user-defined-fields-btn"]',
|
|
logSearchIndexAllFieldBtn: '[data-test="log-search-index-list-interesting-_all-field-btn"]',
|
|
logSearchIndexFieldSearchInput: '[data-test="log-search-index-list-field-search-input"]',
|
|
logSearchIndexFieldsTable: '[data-test="log-search-index-list-fields-table"]',
|
|
logsAllFieldsBtn: '[data-test="logs-all-fields-btn"]',
|
|
logExpandDetailAllKey: '[data-test="log-expand-detail-key-_all"]',
|
|
logsSearchBarQueryEditor: '[data-test="logs-search-bar-query-editor"]',
|
|
logsSearchBarRefreshBtn: '[data-test="logs-search-bar-refresh-btn"]',
|
|
logsSearchErrorMessage: '[data-test="logs-search-error-state"]',
|
|
showQueryToggleBtn: '[data-test="logs-search-bar-show-query-toggle-btn"] div',
|
|
// Stream creation locators
|
|
logStreamAddStreamBtn: '[data-test="log-stream-add-stream-btn"]',
|
|
nameLabel: 'Name *',
|
|
// AddStream.vue migrated from the legacy button to ODrawer's primary footer
|
|
// button — there is no standalone "save-stream-btn" any more.
|
|
saveStreamBtn: '[data-test="add-stream-dialog"] [data-test="o-dialog-primary-btn"]',
|
|
menuHomeItem: '[data-test="menu-link-\\/-item"]',
|
|
menuLogsItem: '[data-test="menu-link-\\/logs-item"]',
|
|
fnEditor: '[data-test="logs-vrl-function-editor"]',
|
|
logSearchIndexSelectStream: '[data-test="log-search-index-list-select-stream-trigger"]',
|
|
logStreamRefreshStatsBtn: '[data-test="log-stream-refresh-stats-btn"]',
|
|
deleteButton: 'Delete',
|
|
okButton: 'Ok',
|
|
// Schema field management locators
|
|
schemaAddFieldsTitle: '[data-test="schema-add-fields-title"]',
|
|
namePlaceholder: 'Name *',
|
|
dataTypePlaceholder: 'Data Type *',
|
|
schemaFieldSearchInput: '[data-test="schema-field-search-input"]',
|
|
schemaDeleteButton: '[data-test="schema-delete-button"]',
|
|
// ConfirmDialog migrated to ODialog — primary footer button
|
|
// is now exposed via the common o-dialog-primary-btn slug.
|
|
confirmButton: '[data-test="o-dialog-primary-btn"]'
|
|
};
|
|
}
|
|
|
|
// =========================================================
|
|
// Factory helpers for runtime-dynamic locators
|
|
// =========================================================
|
|
// Stream row by name in LogStream OTable — uses the deterministic
|
|
// per-name cell data-test rendered via #cell-name template.
|
|
getStreamRowByName(name) {
|
|
return this.page.locator(`[data-test="log-stream-name-cell-${name}"]`);
|
|
}
|
|
|
|
// The per-row delete button (LogStream.vue line 160 → data-test="log-stream-delete-btn").
|
|
// Walk up from the per-name name-cell to the OTable row container, then back down
|
|
// to the delete button (selector kept inside the same row).
|
|
getStreamDeleteButton(name) {
|
|
return this.page
|
|
.locator(`[data-test="log-stream-name-cell-${name}"]`)
|
|
.locator('xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]')
|
|
.locator('[data-test="log-stream-delete-btn"]');
|
|
}
|
|
|
|
// Schema-page row's "schema" (Stream Detail) action button per stream.
|
|
getStreamSchemaButton(name) {
|
|
return this.page
|
|
.locator(`[data-test="log-stream-name-cell-${name}"]`)
|
|
.locator('xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]')
|
|
.locator('[data-test="log-stream-schema-btn"]');
|
|
}
|
|
|
|
// Schema fields table — name-cell per field. schema.vue line 535:
|
|
// `<span :data-test="`schema-field-name-cell-${row.name}`">`.
|
|
getSchemaFieldCell(name) {
|
|
return this.page.locator(`[data-test="schema-field-name-cell-${name}"]`);
|
|
}
|
|
|
|
// Stream picker option (OSelect) by data-test-value
|
|
getStreamOption(stream) {
|
|
return this.page.locator(
|
|
`[data-test="log-search-index-list-select-stream-option"][data-test-value="${stream}"]`,
|
|
);
|
|
}
|
|
|
|
// Get any OSelect option inside the stream selector popover. Used together
|
|
// with the popover-search input to narrow the list and click the first
|
|
// remaining option (works around an OSelect virtualizer bug where the
|
|
// rendered `data-test-value` attribute can lag behind the option's true
|
|
// value when DOM rows are reused on a filter narrow).
|
|
getStreamPopoverOptions() {
|
|
return this.page.locator(
|
|
'[data-test="log-search-index-list-select-stream-popover"] [data-test="log-search-index-list-select-stream-option"]',
|
|
);
|
|
}
|
|
|
|
// Schema new-field data-type option by data-test-value
|
|
getSchemaDataTypeOption(value) {
|
|
return this.page.locator(
|
|
`[data-test="schema-new-field-data-type-select-option"][data-test-value="${value}"], ` +
|
|
`[data-test="add-stream-field-data-type-select-option"][data-test-value="${value}"]`,
|
|
);
|
|
}
|
|
|
|
// Apply query button action
|
|
async applyQuery() {
|
|
testLogger.debug('Applying query with search response wait');
|
|
// OButton returns early in handleClick when loading=true, so wait for enabled first.
|
|
await this.logsSearchBarRefreshBtn.waitFor({ state: 'visible', timeout: 15000 });
|
|
await expect(this.logsSearchBarRefreshBtn).toBeEnabled({ timeout: 15000 });
|
|
const search = this.page.waitForResponse(logData.applyQuery);
|
|
await this.logsSearchBarRefreshBtn.click();
|
|
await expect.poll(async () => (await search).status(), { timeout: 30000 }).toBe(200);
|
|
}
|
|
|
|
// Navigate to streams page
|
|
async navigateToStreams() {
|
|
testLogger.debug('Navigating to streams page');
|
|
await this.menuStreamItem.click();
|
|
}
|
|
|
|
// Search for stream
|
|
async searchStream(streamName) {
|
|
testLogger.debug('Searching for stream', { streamName });
|
|
await this.streamsSearchStreamInputField.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.streamsSearchStreamInputField.click();
|
|
await this.streamsSearchStreamInputField.fill(streamName);
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
// The list refetches 300 ms after typing and swaps its rows for a skeleton, which swallows an earlier row click.
|
|
const needle = streamName.toLowerCase();
|
|
await expect.poll(async () => {
|
|
const names = await this.logStreamNameCells.evaluateAll((cells) => cells.map((cell) => cell.getAttribute('data-test')));
|
|
return names.length > 0 && names.every((name) => name.toLowerCase().includes(needle));
|
|
}, { timeout: 15000 }).toBe(true);
|
|
}
|
|
|
|
// Open stream details
|
|
async openStreamDetails() {
|
|
testLogger.debug('Opening stream details');
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Find the schema (Stream Detail) action button on the first visible row of the table.
|
|
// We can't rely on a per-name button (LogStream.vue cell-actions buttons have a generic
|
|
// data-test), so we use `.first()` which deterministically resolves to the leading row
|
|
// after the stream search filter narrows the OTable down to one row.
|
|
await this.logStreamSchemaBtnFirst.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.logStreamSchemaBtnFirst.click();
|
|
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Configure schema fields
|
|
async configureSchemaFields() {
|
|
testLogger.debug('Configuring schema fields - removing kubernetes annotations if present');
|
|
|
|
// Ensure we're in the right tab context
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Try to find and click kubernetes checkboxes (they may not always exist).
|
|
// These names are legacy from the old table schema; if the corresponding
|
|
// data-tests no longer exist after the OTable migration the row checkboxes
|
|
// are simply not found and the try-block swallows the timeout.
|
|
try {
|
|
const kubernetesRow = this.getSchemaFieldCell('kubernetes_annotations_kubectl_kubernetes_io_default_container')
|
|
.first()
|
|
.locator('xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]')
|
|
.locator('[data-test="o2-table-row-checkbox"]');
|
|
if (await kubernetesRow.isVisible({ timeout: 5000 })) {
|
|
await kubernetesRow.click();
|
|
testLogger.debug('Clicked kubernetes default container checkbox');
|
|
}
|
|
} catch (error) {
|
|
testLogger.debug('Kubernetes default container checkbox not found or not clickable');
|
|
}
|
|
|
|
try {
|
|
const kubernetesPspRow = this.getSchemaFieldCell('kubernetes_annotations_kubernetes_io_psp')
|
|
.first()
|
|
.locator('xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]')
|
|
.locator('[data-test="o2-table-row-checkbox"]');
|
|
if (await kubernetesPspRow.isVisible({ timeout: 5000 })) {
|
|
await kubernetesPspRow.click();
|
|
testLogger.debug('Clicked kubernetes psp checkbox');
|
|
}
|
|
} catch (error) {
|
|
testLogger.debug('Kubernetes psp checkbox not found or not clickable');
|
|
}
|
|
|
|
// Always try to add field and update settings
|
|
await this.schemaAddFieldButton.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.schemaAddFieldButton.click();
|
|
|
|
await this.schemaUpdateSettingsButton.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.schemaUpdateSettingsButton.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Navigate to schema fields tab
|
|
async navigateToSchemaFieldsTab() {
|
|
testLogger.debug('Navigating to schema fields tab');
|
|
// OToggleGroupItem doesn't carry its own data-test; locate by parent wrapper
|
|
// and ToggleGroupRoot value via xpath data-* attribute.
|
|
const tab = this.schemaFieldsTabs.locator('[data-state][value="schemaFields"]');
|
|
await tab.waitFor({ state: 'visible' });
|
|
await tab.click();
|
|
}
|
|
|
|
// Navigate to all fields tab
|
|
async navigateToAllFieldsTab() {
|
|
testLogger.debug('Navigating to all fields tab');
|
|
const tab = this.schemaFieldsTabs.locator('[data-state][value="allFields"]');
|
|
await tab.waitFor({ state: 'visible' });
|
|
await tab.click();
|
|
}
|
|
|
|
// Verify kubernetes annotation fields are visible
|
|
async verifyKubernetesAnnotationFields() {
|
|
testLogger.debug('Verifying kubernetes annotation fields visibility');
|
|
await this.getSchemaFieldCell('kubernetes_annotations_kubectl_kubernetes_io_default_container').click();
|
|
await this.getSchemaFieldCell('kubernetes_annotations_kubernetes_io_psp').click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Close dialog
|
|
async closeDialog() {
|
|
testLogger.debug('Closing dialog');
|
|
await this.schemaDrawerCloseBtn.click();
|
|
}
|
|
|
|
// Explore stream
|
|
async exploreStream() {
|
|
testLogger.debug('Exploring stream');
|
|
// LogStream OTable per-row explore button (data-test="log-stream-explore-btn")
|
|
await this.logStreamExploreBtnFirst.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Configure date time settings
|
|
async configureDateTimeSettings() {
|
|
testLogger.debug('Configuring date time settings');
|
|
await this.dateTimeBtn.click();
|
|
await this.dateTimeRelativeTab.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Expand first log row
|
|
async expandFirstLogRow() {
|
|
testLogger.debug('Expanding first log row');
|
|
await this.logTableExpandMenu.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Configure user defined fields
|
|
async configureUserDefinedFields() {
|
|
testLogger.debug('Configuring user defined fields');
|
|
await this.logsUserDefinedFieldsBtn.click();
|
|
await this.logSearchIndexAllFieldBtn.last().click({ force: true });
|
|
}
|
|
|
|
// Expand all dropdown and search fields
|
|
async expandAllDropdownAndSearchFields() {
|
|
testLogger.debug('Expanding all dropdown and searching for fields');
|
|
// Toggle the `_all` field group expander via the field-expand data-test
|
|
await this.logSearchExpandAllField.click();
|
|
await this.logSearchIndexFieldSearchInput.click();
|
|
await this.logSearchIndexFieldSearchInput.fill('_timestamp');
|
|
await this.logSearchIndexFieldsTableTimestampField.click();
|
|
await this.logSearchIndexFieldSearchInput.click();
|
|
await this.logSearchIndexFieldSearchInput.fill('_all');
|
|
await this.logSearchIndexFieldsTableAllField.waitFor({ state: 'visible' });
|
|
}
|
|
|
|
// Navigate to all fields
|
|
async navigateToAllFields() {
|
|
testLogger.debug('Navigating to all fields');
|
|
await this.logsAllFieldsBtn.click();
|
|
await this.logSearchIndexFieldSearchInput.click();
|
|
await this.logSearchIndexFieldsTableAllField.waitFor({ state: 'visible' });
|
|
await this.logSearchIndexFieldSearchInput.click();
|
|
await this.logSearchIndexFieldSearchInput.fill('_timestamp');
|
|
await this.logSearchIndexFieldsTableTimestampField.click();
|
|
}
|
|
|
|
// Wait for _all field and add query
|
|
async waitForAllFieldAndAddQuery() {
|
|
testLogger.debug('Waiting for _all field and adding query');
|
|
await this.logExpandDetailAllKey.waitFor({ state: 'visible' });
|
|
// Drive the Monaco editor via window.monaco — clicking `.monaco-editor` is banned (§2)
|
|
await this.page.waitForFunction(
|
|
() => Boolean(window.monaco?.editor?.getEditors()?.length),
|
|
undefined,
|
|
{ timeout: 15000 },
|
|
);
|
|
await this.page.evaluate((text) => {
|
|
const editor = window.monaco.editor.getEditors()[0];
|
|
if (editor) editor.setValue(text);
|
|
}, "str_match(_all, 'test')");
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Run query and verify no error
|
|
async runQueryAndVerifyNoError() {
|
|
testLogger.debug('Running query and verifying no error');
|
|
await this.logsSearchBarRefreshBtn.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
await expect(this.logsSearchErrorMessage).not.toBeVisible();
|
|
}
|
|
|
|
// Click timestamp dropdown
|
|
async clickTimestampDropdown() {
|
|
testLogger.debug('Clicking timestamp dropdown');
|
|
// Use the field-row expand-toggle data-test instead of plain-text matching
|
|
await this.logSearchExpandTimestampField.first().click();
|
|
}
|
|
|
|
// Create new stream
|
|
async createNewStream(streamName) {
|
|
testLogger.debug('Creating new stream', { streamName });
|
|
await this.logStreamAddStreamBtn.click();
|
|
// ODrawer transitions in — wait for the dialog wrapper to be visible.
|
|
await this.addStreamDialog.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.addStreamNameInputField.click();
|
|
await this.addStreamNameInputField.fill(streamName);
|
|
// OSelect popover: open trigger, pick the `logs` option by its data-test-value
|
|
await this.addStreamTypeInput.click();
|
|
await this.addStreamTypeOptionLogs.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.addStreamTypeOptionLogs.click();
|
|
await this.addStreamSaveBtn.click();
|
|
// Wait for the drawer to close after a successful save (ODrawer is removed from DOM).
|
|
await this.addStreamDialog.waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {});
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Navigate home then logs
|
|
//
|
|
// Performs a hard reload after landing on /web/logs so the Vuex streams
|
|
// cache is dropped — without the reload, the OSelect dropdown pulls from
|
|
// the stale list that was fetched on the first /web/logs visit (before
|
|
// any stream created by this test existed). The reload triggers a fresh
|
|
// streams-list API call so the new blank stream appears as an option.
|
|
async navigateHomeThenLogs() {
|
|
testLogger.debug('Navigating home then to logs');
|
|
await this.menuHomeItem.click();
|
|
await this.menuLogsItem.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
await this.page.reload();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Switch stream in logs
|
|
async switchStreamInLogs(fromStream, toStream) {
|
|
testLogger.debug('Switching stream in logs', { fromStream, toStream });
|
|
|
|
// Check if VRL editor is available
|
|
const fnEditorCount = await this.fnEditor.count();
|
|
|
|
if (fnEditorCount === 0) {
|
|
|
|
// Try to reload the page with VRL editor enabled
|
|
const currentUrl = new URL(this.page.url());
|
|
currentUrl.searchParams.set('fn_editor', 'true');
|
|
currentUrl.searchParams.set('vrl', 'true');
|
|
|
|
await this.page.goto(currentUrl.toString());
|
|
await this.page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
|
|
const fnEditorCountAfterReload = await this.fnEditor.count();
|
|
|
|
if (fnEditorCountAfterReload === 0) {
|
|
// Skip the VRL editor click and try alternative approach
|
|
await this._pickStreamOption(fromStream);
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
await this._pickStreamOption(toStream);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
// Wait for the spinner / loading state to dissolve before continuing.
|
|
await this.logsSearchResultLoading.waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {});
|
|
return; // Exit early, skipping VRL editor interaction
|
|
}
|
|
}
|
|
|
|
// Try clicking the monaco editor by activating it through the show-query toggle.
|
|
// The previous `.monaco-editor` CSS-class click is banned (§2). Activate the
|
|
// editor by opening the function editor area via window.monaco focus.
|
|
try {
|
|
await this.fnEditor.first().waitFor({ state: 'visible', timeout: 3000 });
|
|
} catch (error) {
|
|
testLogger.warn('Failed to find monaco editor visible, trying toggle button');
|
|
|
|
// The show-query toggle moved into the utilities ODropdown in SearchBar.vue.
|
|
// Check if it is directly visible (FunctionSelector/TransformSelector path);
|
|
// if not, open the utilities menu first and click the dropdown item.
|
|
const isToggleDirectlyVisible = await this.showQueryToggleBtn.first().isVisible().catch(() => false);
|
|
if (isToggleDirectlyVisible) {
|
|
await this.showQueryToggleBtn.first().click({ force: true });
|
|
} else {
|
|
await this.utilitiesMenuBtn.click();
|
|
await this.menuTransformEditorToggleBtn.waitFor({ state: 'visible', timeout: 5000 });
|
|
await this.menuTransformEditorToggleBtn.click();
|
|
}
|
|
await this.fnEditor.first().waitFor({ state: 'visible', timeout: 15000 });
|
|
}
|
|
await this._pickStreamOption(fromStream);
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
await this._pickStreamOption(toStream);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Dismiss the OSelect popover with Escape and confirm by waiting on its detachment.
|
|
await this.page.keyboard.press('Escape').catch(() => {});
|
|
// Wait until the timestamp field shows up in the stream-fields table, signalling
|
|
// the new stream's schema has been loaded.
|
|
await this.logSearchIndexFieldsTable
|
|
.locator('[data-test="log-search-expand-_timestamp-field"]')
|
|
.waitFor({ state: 'visible', timeout: 15000 })
|
|
.catch(() => {});
|
|
}
|
|
|
|
// Helper: open the OSelect stream popover and click the option whose
|
|
// data-test-value matches the requested stream name. Used by switchStreamInLogs
|
|
// (replaces the legacy fill + getByText pattern).
|
|
//
|
|
// Uses the OSelect popover-search pattern: open trigger → wait popover →
|
|
// clear search input (Ctrl/Meta+a, Backspace) → fill stream name → wait
|
|
// until the filtered popover narrows to >=1 option → click the first option.
|
|
// The first-option strategy works around an OSelect virtualizer bug where
|
|
// the rendered `data-test-value` attribute can lag behind the option's
|
|
// true value when DOM rows are reused on a filter narrow.
|
|
async _pickStreamOption(stream) {
|
|
// Wait for the trigger to be in a stable closed state before clicking.
|
|
// Reka popover transitions take ~150-200ms; allow up to 5s for full
|
|
// settlement (animation + state attribute flip).
|
|
await expect(this.logSearchIndexSelectStream)
|
|
.toHaveAttribute('aria-expanded', 'false', { timeout: 5000 })
|
|
.catch(() => {});
|
|
|
|
// Open the popover with retry. Reka PopoverTrigger toggles state on
|
|
// each click; if the previous _pickStreamOption left the popover open
|
|
// (multi-mode + Escape race), the first click here would CLOSE it.
|
|
// Retry the click up to 3 times until we see the popover visible.
|
|
let popoverOpen = false;
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
await this.logSearchIndexSelectStream.click();
|
|
// Small animation grace — Reka popover slide-in animation needs
|
|
// ~200ms to attach to DOM (user-approved waitForTimeout for
|
|
// animation races, AGENT_RULES §10 override).
|
|
await this.page.waitForTimeout(300);
|
|
popoverOpen = await this.logSearchStreamPopover
|
|
.isVisible({ timeout: 3000 })
|
|
.catch(() => false);
|
|
if (popoverOpen) break;
|
|
testLogger.warn(`[_pickStreamOption] popover not visible after click attempt ${attempt}, retrying`);
|
|
// If trigger toggled the wrong way, press Escape to force-close
|
|
// and try again on the next iteration.
|
|
await this.page.keyboard.press('Escape').catch(() => {});
|
|
await this.page.waitForTimeout(200);
|
|
}
|
|
// Final hard wait for visibility — if all retries failed this throws
|
|
// with the original timeout error and CI screenshots capture state.
|
|
await this.logSearchStreamPopover.waitFor({ state: 'visible', timeout: 10000 });
|
|
|
|
// Defensive clear via Ctrl/Meta+A → Backspace handles reka-ui's
|
|
// ComboboxInput internal searchTerm state surviving a `.fill()` overwrite.
|
|
await this.logSearchStreamPopoverSearch.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.logSearchStreamPopoverSearch.press('ControlOrMeta+a');
|
|
await this.logSearchStreamPopoverSearch.press('Backspace');
|
|
await this.logSearchStreamPopoverSearch.fill(stream);
|
|
|
|
// Wait until the filtered popover has at least one option (the typed
|
|
// stream name should narrow the list to just our stream).
|
|
await expect.poll(
|
|
async () => await this.getStreamPopoverOptions().count().catch(() => -1),
|
|
{ intervals: [500, 1000, 2000], timeout: 20000 },
|
|
).toBeGreaterThanOrEqual(1);
|
|
|
|
const option = this.getStreamPopoverOptions().first();
|
|
await option.waitFor({ state: 'visible', timeout: 10000 });
|
|
await option.click();
|
|
// In multiple/listbox mode the popover stays open after option click —
|
|
// deterministically dismiss it and wait for the trigger to report
|
|
// `aria-expanded="false"` so the next _pickStreamOption call can safely
|
|
// open it again. Without this gate, the next trigger click toggles the
|
|
// still-open popover to closed and the popover-visible wait times out.
|
|
await this.page.keyboard.press('Escape').catch(() => {});
|
|
// Allow Reka close animation to flush before aria-expanded check.
|
|
await this.page.waitForTimeout(300);
|
|
await expect(this.logSearchIndexSelectStream)
|
|
.toHaveAttribute('aria-expanded', 'false', { timeout: 5000 })
|
|
.catch(() => {});
|
|
}
|
|
|
|
// Refresh stream stats and delete
|
|
async refreshStreamStatsAndDelete(streamName) {
|
|
testLogger.debug('Refreshing stream stats and deleting stream', { streamName });
|
|
await this.streamsSearchStreamInputField.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.streamsSearchStreamInputField.click();
|
|
await this.streamsSearchStreamInputField.fill(streamName);
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
await this.logStreamRefreshStatsBtn.click();
|
|
await this.streamsSearchStreamInputField.click();
|
|
// Re-focus the search input to keep the filter in place
|
|
await this.streamsSearchStreamInputField.click();
|
|
|
|
// Wait for the specific stream row (by the per-name name-cell) and click its delete button
|
|
const nameCell = this.getStreamRowByName(streamName);
|
|
await nameCell.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.getStreamDeleteButton(streamName).click();
|
|
// ODialog confirmation — primary OK button inside the log-stream-delete-dialog wrapper.
|
|
await this.logStreamDeleteDialog.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.logStreamDeleteDialogOk.click();
|
|
await this.logStreamDeleteDialog.waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
// Add new schema field
|
|
async addNewSchemaField(fieldName) {
|
|
testLogger.debug('Adding new schema field', { fieldName });
|
|
await this.schemaAddFieldsTitle.click();
|
|
// StreamFieldsInputs renders OInput rows; the first row's name field carries
|
|
// a generic OInput data-test forwarded to the inner field.
|
|
await this.schemaNewFieldNameInputField.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.schemaNewFieldNameInputField.click();
|
|
await this.schemaNewFieldNameInputField.fill(fieldName);
|
|
}
|
|
|
|
// Search for schema field
|
|
async searchSchemaField(fieldName) {
|
|
testLogger.debug('Searching for schema field', { fieldName });
|
|
await this.schemaFieldSearchInputField.fill(fieldName);
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Delete schema field by checkbox
|
|
async deleteSchemaFieldByCheckbox(fieldName) {
|
|
testLogger.debug('Deleting schema field by checkbox', { fieldName });
|
|
// OTable selection checkbox lives in the row matched by the schema-field-name-cell.
|
|
const row = this.getSchemaFieldCell(fieldName)
|
|
.locator('xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]');
|
|
await row.locator('[data-test="o2-table-row-checkbox"]').click();
|
|
await this.schemaAddFieldButton.click();
|
|
await this.schemaUpdateSettingsButton.click();
|
|
}
|
|
|
|
// Delete schema field permanently
|
|
async deleteSchemaFieldPermanently(fieldName) {
|
|
testLogger.debug('Deleting schema field permanently', { fieldName });
|
|
// Click the field's name cell to select / focus it, then check the row checkbox.
|
|
const nameCell = this.getSchemaFieldCell(fieldName);
|
|
await nameCell.first().click();
|
|
const row = nameCell.first()
|
|
.locator('xpath=ancestor::*[starts-with(@data-test,"o2-table-row-")]');
|
|
await row.locator('[data-test="o2-table-row-checkbox"]').first().click();
|
|
await this.schemaDeleteButton.click();
|
|
// Confirm dialog primary button — try the scoped confirm-dialog wrapper first,
|
|
// fall back to the generic ODialog primary button.
|
|
const scopedOk = this.confirmDialogPrimaryBtn;
|
|
if (await scopedOk.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await scopedOk.click();
|
|
} else {
|
|
await this.dialogPrimaryBtn.first().click();
|
|
}
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
await this.schemaDrawerCloseBtn.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Select random data type for schema field
|
|
async selectRandomDataType() {
|
|
testLogger.debug('Selecting random data type');
|
|
// Open the data-type OSelect — wrapper carries data-test on the row input
|
|
await this.schemaNewFieldDataTypeSelect.click();
|
|
const dataTypeValues = ['Float64', 'Int64', 'Boolean'];
|
|
const choice = dataTypeValues[Math.floor(Math.random() * dataTypeValues.length)];
|
|
const option = this.getSchemaDataTypeOption(choice);
|
|
await option.first().waitFor({ state: 'visible', timeout: 10000 });
|
|
await option.first().click();
|
|
}
|
|
|
|
// Complete schema field workflow for stream settings test
|
|
async completeStreamSettingsSchemaWorkflow(testStreamName) {
|
|
testLogger.debug('Completing stream settings schema workflow', { testStreamName });
|
|
|
|
try {
|
|
// Minimal workflow: Just verify we can navigate to streams and open details
|
|
await this.navigateToStreams();
|
|
await this.searchStream(testStreamName);
|
|
await this.openStreamDetails();
|
|
|
|
// Basic schema functionality - just verify the interface loads
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
testLogger.debug('Schema interface loaded successfully');
|
|
|
|
// Try to find any schema-related element to confirm we're in the right place
|
|
const schemaElements = await this.schemaElementCountLocator.count();
|
|
testLogger.debug(`Found ${schemaElements} schema-related elements`);
|
|
|
|
testLogger.debug('Schema workflow completed successfully');
|
|
|
|
} catch (error) {
|
|
testLogger.error('Error in stream settings schema workflow', { error: error.message, testStreamName });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Complete blank stream to detailed stream workflow
|
|
async completeBlankToDetailedStreamWorkflow(blankStreamName, detailedStreamName) {
|
|
testLogger.debug('Completing blank to detailed stream workflow', { blankStreamName, detailedStreamName });
|
|
|
|
// Navigate and create new stream
|
|
await this.navigateToStreams();
|
|
await this.createNewStream(blankStreamName);
|
|
|
|
// Navigate to logs and switch streams
|
|
await this.navigateHomeThenLogs();
|
|
await this.switchStreamInLogs(blankStreamName, detailedStreamName);
|
|
|
|
// Return to streams and cleanup
|
|
await this.navigateToStreams();
|
|
await this.refreshStreamStatsAndDelete(blankStreamName);
|
|
}
|
|
|
|
// Complete add and delete field workflow
|
|
async completeAddAndDeleteFieldWorkflow(testStreamName) {
|
|
testLogger.debug('Completing add and delete field workflow', { testStreamName });
|
|
|
|
try {
|
|
// Step 1: Navigate to streams
|
|
testLogger.debug('Step 1: Navigating to streams');
|
|
await this.navigateToStreams();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
|
|
// Step 2: Search for stream
|
|
testLogger.debug('Step 2: Searching for stream', { testStreamName });
|
|
await this.searchStream(testStreamName);
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
|
|
// Step 3: Open stream details with error checking
|
|
testLogger.debug('Step 3: Opening stream details');
|
|
await this.openStreamDetails();
|
|
|
|
// Check if page is still alive after navigation
|
|
if (this.page.isClosed()) {
|
|
throw new Error('Page was closed after opening stream details');
|
|
}
|
|
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
testLogger.debug('Stream details opened successfully');
|
|
|
|
// Step 4: Check what tabs are actually available and find the right one
|
|
testLogger.debug('Step 4: Investigating available tabs');
|
|
|
|
// Get all available tabs (OToggleGroupItem doesn't carry data-test; query the
|
|
// wrapper data-test="schema-fields-tabs" to confirm at least one toggle group
|
|
// exists). Tabs are referenced by `[value="<name>"]` attribute on the inner button.
|
|
const tabsExist = await this.schemaFieldsTabs.count();
|
|
testLogger.debug('Schema fields tabs wrapper count', { count: tabsExist });
|
|
|
|
// Try to use any available tab that allows field operations
|
|
let tabFound = false;
|
|
|
|
// Try allFields first
|
|
const allFieldsTab = this.schemaFieldsTabs.locator('[value="allFields"]');
|
|
if (await allFieldsTab.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await this.navigateToAllFieldsTab();
|
|
tabFound = true;
|
|
testLogger.debug('Using allFields tab');
|
|
}
|
|
// Try schemaFields as fallback
|
|
else {
|
|
const schemaFieldsTab = this.schemaFieldsTabs.locator('[value="schemaFields"]');
|
|
if (await schemaFieldsTab.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await this.navigateToSchemaFieldsTab();
|
|
tabFound = true;
|
|
testLogger.debug('Using schemaFields tab');
|
|
}
|
|
}
|
|
|
|
if (!tabFound) {
|
|
testLogger.debug('No specific tab found, proceeding without tab navigation');
|
|
}
|
|
|
|
// Step 5: Verify we can see field-related functionality
|
|
testLogger.debug('Step 5: Checking field operation availability');
|
|
|
|
// Just verify the basic schema interface is working
|
|
const schemaElements = await this.schemaElementCountLocator.count();
|
|
testLogger.debug(`Found ${schemaElements} schema-related elements`);
|
|
|
|
// Check if add field functionality is present
|
|
const addFieldButton = await this.schemaAddFieldButton.isVisible({ timeout: 2000 }).catch(() => false);
|
|
testLogger.debug(`Add field button visible: ${addFieldButton}`);
|
|
|
|
// Basic success - we can navigate to stream details and see schema interface
|
|
testLogger.debug('Schema field workflow access verified successfully');
|
|
|
|
} catch (error) {
|
|
testLogger.error('Error in add and delete field workflow', {
|
|
error: error.message,
|
|
testStreamName,
|
|
pageState: this.page.isClosed() ? 'CLOSED' : 'OPEN',
|
|
currentUrl: this.page.isClosed() ? 'unknown' : await this.page.url()
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// =========================================================
|
|
// Timeline chip interaction methods
|
|
// =========================================================
|
|
|
|
// Wait for the schema drawer to become visible
|
|
async waitForSchemaDrawerVisible() {
|
|
await this.schemaDrawer.waitFor({ state: 'visible', timeout: 30000 });
|
|
}
|
|
|
|
// Wait for the timeline chip to appear inside the schema drawer
|
|
async waitForTimelineChipVisible() {
|
|
// First ensure the drawer is visible and schema has loaded (spinner gone)
|
|
await this.schemaDrawer.waitFor({ state: 'visible', timeout: 30000 });
|
|
// The timeline chip only renders when indexData.name is set (after getSchema() API)
|
|
await this.schemaTimelineChip.waitFor({ state: 'visible', timeout: 30000 });
|
|
}
|
|
|
|
// Expect the timeline chip to be visible
|
|
async expectTimelineChipVisible() {
|
|
await expect(this.schemaTimelineChip).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
// Get the text content of the timezone label span
|
|
async getTimezoneLabelText() {
|
|
const text = await this.schemaTimelineTimezoneLabel.textContent();
|
|
return (text || '').trim();
|
|
}
|
|
|
|
// Get the text content of the doc_time range (min → max)
|
|
async getDocTimeRangeText() {
|
|
const text = await this.schemaTimelineDocTimeRange.textContent();
|
|
return (text || '').trim();
|
|
}
|
|
|
|
// Assert the timezone label is non-empty
|
|
async expectTimezoneLabelNonEmpty() {
|
|
await expect(this.schemaTimelineTimezoneLabel).toBeVisible({ timeout: 15000 });
|
|
const text = await this.schemaTimelineTimezoneLabel.textContent();
|
|
expect(text).toBeTruthy();
|
|
expect(text.trim().length).toBeGreaterThan(0);
|
|
}
|
|
|
|
// Assert the doc_time range is a formatted date (not raw epoch)
|
|
async expectDocTimeRangeFormatted() {
|
|
await expect(this.schemaTimelineDocTimeRange).toBeVisible({ timeout: 15000 });
|
|
const text = await this.schemaTimelineDocTimeRange.textContent();
|
|
expect(text).toBeTruthy();
|
|
// Should NOT contain epoch date (1970-01-01) — stream has ingested data
|
|
expect(text).not.toContain('1970-01-01');
|
|
// Should contain a formatted timestamp pattern like YYYY-MM-DD
|
|
expect(text).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
}
|
|
|
|
// Click the Schema Settings tab
|
|
async clickSchemaSettingsTab() {
|
|
await this.schemaSettingsTab.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.schemaSettingsTab.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Click the Configuration tab
|
|
async clickSchemaConfigurationTab() {
|
|
await this.schemaConfigurationTab.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.schemaConfigurationTab.click();
|
|
await this.page.waitForLoadState('domcontentloaded');
|
|
}
|
|
|
|
// Verification methods for POM compliance
|
|
async verifyNoErrorMessages() {
|
|
await expect(this.logsSearchErrorMessage).not.toBeVisible();
|
|
}
|
|
|
|
async verifyStreamsPageAccessible() {
|
|
await this.menuStreamItem.click();
|
|
const streamsPageVisible = await this.streamsSearchStreamInputField.isVisible();
|
|
expect(streamsPageVisible).toBe(true);
|
|
return streamsPageVisible;
|
|
}
|
|
|
|
/**
|
|
* Assert the stream's schema editor actually rendered: the schema ODrawer is
|
|
* open, its title is shown, and the field-mapping table (the stream's field
|
|
* list) is visible. This is the real, environment-independent outcome of
|
|
* opening a stream's schema detail — the add-field / delete-field controls
|
|
* are gated behind isSchemaUDSEnabled (User-Defined Schema, enterprise/config)
|
|
* so they are NOT a reliable cross-environment signal.
|
|
*/
|
|
async verifySchemaEditorLoaded() {
|
|
await expect(this.schemaDrawer).toBeVisible({ timeout: 30000 });
|
|
await expect(this.schemaTitleText).toBeVisible({ timeout: 15000 });
|
|
await expect(this.schemaLogStreamFieldMappingTable.first()).toBeVisible({ timeout: 15000 });
|
|
}
|
|
}
|
|
|
|
module.exports = SchemaPage;
|