mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
308 lines
13 KiB
JavaScript
308 lines
13 KiB
JavaScript
// Copyright 2026 OpenObserve Inc.
|
|
|
|
const { expect } = require('@playwright/test');
|
|
const testLogger = require('../../playwright-tests/utils/test-logger.js');
|
|
|
|
class FunctionsFormValidationPage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// ==================== AddFunction / FunctionsToolbar locators ====================
|
|
|
|
// Name input (OInput — derives -field and -error automatically)
|
|
this.functionNameField = page.locator('[data-test="add-function-name-input-field"]');
|
|
this.functionNameError = page.locator('[data-test="add-function-name-input-error"]');
|
|
|
|
// VRL editor wrapper
|
|
this.functionEditor = page.locator('[data-test="logs-vrl-function-editor"]');
|
|
|
|
// Action buttons
|
|
this.saveButton = page.locator('[data-test="add-function-save-btn"]');
|
|
this.cancelButton = page.locator('[data-test="add-function-cancel-btn"]');
|
|
this.backButton = page.locator('[data-test="add-function-back-btn"]');
|
|
|
|
// Toast notifications
|
|
this.toastSuccess = page.locator('[data-test-variant="success"]');
|
|
this.toastError = page.locator('[data-test-variant="error"]');
|
|
|
|
// ==================== AddEnrichmentTable locators ====================
|
|
|
|
// Add form root
|
|
this.enrichmentPage = page.locator('[data-test="add-enrichment-table-page"]');
|
|
|
|
// Name field (OInput)
|
|
this.enrichmentNameField = page.locator('[data-test="add-enrichment-table-name-field"]');
|
|
this.enrichmentNameError = page.locator('[data-test="add-enrichment-table-name-error"]');
|
|
|
|
// File upload (OFile — derives -field automatically; -error for validation msg)
|
|
this.enrichmentFileField = page.locator('[data-test="add-enrichment-table-file-field"]');
|
|
this.enrichmentFileError = page.locator('[data-test="add-enrichment-table-file-error"]');
|
|
|
|
// URL field (OInput for URL source)
|
|
this.enrichmentUrlField = page.locator('[data-test="add-enrichment-table-url-field"]');
|
|
this.enrichmentUrlError = page.locator('[data-test="add-enrichment-table-url-error"]');
|
|
|
|
// Source toggle (OOptionGroup — file vs url)
|
|
this.enrichmentSourceGroup = page.locator('[data-test="add-enrichment-table-source"]');
|
|
|
|
// Footer buttons
|
|
this.enrichmentSaveButton = page.locator('[data-test="add-enrichment-table-save-btn"]');
|
|
this.enrichmentCancelButton = page.locator('[data-test="add-enrichment-table-cancel-btn"]');
|
|
this.enrichmentBackButton = page.locator('[data-test="add-enrichment-table-back-btn"]');
|
|
|
|
// Add enrichment button on list page
|
|
this.enrichmentAddButton = page.locator('[data-test="enrichment-tables-add-btn"]');
|
|
|
|
// Pipeline / enrichment navigation
|
|
this.enrichmentTableTab = page.locator('[data-test="pipeline-section-tab-enrichmentTables"]');
|
|
|
|
// Add function button on functions list
|
|
this.addFunctionButton = page.locator('[data-test="function-list-add-function-btn"]');
|
|
|
|
// ==================== FunctionsToolbar locators ====================
|
|
|
|
// VRL / JS transform type radios
|
|
this.vrlRadio = page.locator('[data-test="function-transform-type-vrl-radio"]');
|
|
this.jsRadio = page.locator('[data-test="function-transform-type-js-radio"]');
|
|
|
|
// ==================== StreamRouting locators ====================
|
|
|
|
// StreamRouting panel root
|
|
this.streamRoutingSection = page.locator('[data-test="add-stream-routing-section"]');
|
|
|
|
// Close icon
|
|
this.streamRoutingCloseBtn = page.locator('[data-test="stream-routing-close-dialog-btn"]');
|
|
|
|
// Destination name input (OInput — derives -field and -error)
|
|
this.streamRoutingDestNameField = page.locator('[data-test="stream-routing-destination-name-input-field"]');
|
|
this.streamRoutingDestNameError = page.locator('[data-test="stream-routing-destination-name-input-error"]');
|
|
|
|
// Stream type select (OSelect — derives -error)
|
|
this.streamRoutingStreamTypeError = page.locator('[data-test="add-alert-stream-type-select-error"]');
|
|
|
|
// Stream name select (OSelect — derives -error)
|
|
this.streamRoutingStreamNameError = page.locator('[data-test="add-alert-stream-select-error"]');
|
|
|
|
// Save / Cancel buttons
|
|
this.streamRoutingSaveBtn = page.locator('[data-test="add-report-save-btn"]');
|
|
this.streamRoutingCancelBtn = page.locator('[data-test="add-report-cancel-btn"]');
|
|
|
|
// Pipeline add-routing button (if available in the pipeline view)
|
|
this.addStreamRoutingButton = page.locator('[data-test="pipeline-add-stream-routing-btn"]');
|
|
}
|
|
|
|
// ==================== Navigation ====================
|
|
|
|
async navigateToFunctions(org) {
|
|
const targetOrg = org || process.env.ORGID;
|
|
testLogger.info(`Navigating to functions page for org: ${targetOrg}`);
|
|
await this.page.goto(
|
|
`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${targetOrg}`
|
|
);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
}
|
|
|
|
async navigateToEnrichmentTables(org) {
|
|
const targetOrg = org || process.env.ORGID;
|
|
testLogger.info(`Navigating to enrichment tables for org: ${targetOrg}`);
|
|
await this.page.goto(
|
|
`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${targetOrg}`
|
|
);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
await this.enrichmentTableTab.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.enrichmentTableTab.click();
|
|
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
}
|
|
|
|
// ==================== Add Function actions ====================
|
|
|
|
async openAddFunctionForm(org) {
|
|
await this.navigateToFunctions(org);
|
|
await this.addFunctionButton.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.addFunctionButton.click();
|
|
await this.functionNameField.waitFor({ state: 'visible', timeout: 15000 });
|
|
testLogger.info('Add Function form opened');
|
|
}
|
|
|
|
async fillFunctionName(name) {
|
|
testLogger.info(`Filling function name: ${name}`);
|
|
await this.functionNameField.waitFor({ state: 'visible' });
|
|
await this.functionNameField.fill(name);
|
|
}
|
|
|
|
async clickSave() {
|
|
testLogger.info('Clicking Save button');
|
|
await this.saveButton.waitFor({ state: 'visible' });
|
|
await this.saveButton.click();
|
|
}
|
|
|
|
async expectFunctionNameError(message) {
|
|
testLogger.info(`Expecting function name error: ${message}`);
|
|
await this.functionNameError.waitFor({ state: 'visible', timeout: 5000 });
|
|
await expect(this.functionNameError).toBeVisible();
|
|
if (message) {
|
|
await expect(this.functionNameError).toContainText(message);
|
|
}
|
|
}
|
|
|
|
async expectFunctionNameErrorVisible() {
|
|
testLogger.info('Expecting function name error to be visible');
|
|
await this.functionNameError.waitFor({ state: 'visible', timeout: 5000 });
|
|
await expect(this.functionNameError).toBeVisible();
|
|
}
|
|
|
|
async typeInVrlEditor(code) {
|
|
testLogger.info('Typing VRL code into editor');
|
|
await this.functionEditor.waitFor({ state: 'visible', timeout: 15000 });
|
|
// Click the editor to ensure Monaco is focused/initialized (lazy-loaded async component)
|
|
await this.functionEditor.click();
|
|
await this.page.waitForTimeout(300);
|
|
// Use Monaco's executeEdits API to set the content and trigger onDidChangeModelContent,
|
|
// which propagates through unified-query-editor's @update:query to formData.function.
|
|
// keyboard.type() and keyboard.insertText() do NOT trigger the reactive update.
|
|
await this.page.evaluate((vrlCode) => {
|
|
const editors = window.monaco?.editor?.getEditors?.();
|
|
const container = document.querySelector('[data-test="logs-vrl-function-editor"]');
|
|
const targetEditor = editors?.find(e => container?.contains(e.getContainerDomNode()));
|
|
if (targetEditor) {
|
|
targetEditor.executeEdits('playwright', [{
|
|
range: targetEditor.getModel().getFullModelRange(),
|
|
text: vrlCode
|
|
}]);
|
|
}
|
|
}, code);
|
|
// Wait for Vue reactivity to propagate from onDidChangeModelContent → emit → formData.function
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
async expectSuccessToast() {
|
|
testLogger.info('Waiting for success toast');
|
|
await this.toastSuccess.waitFor({ state: 'visible', timeout: 15000 });
|
|
await expect(this.toastSuccess).toBeVisible();
|
|
}
|
|
|
|
// ==================== Add Enrichment Table actions ====================
|
|
|
|
async openAddEnrichmentTableForm(org) {
|
|
await this.navigateToEnrichmentTables(org);
|
|
await this.enrichmentAddButton.waitFor({ state: 'visible', timeout: 15000 });
|
|
await this.enrichmentAddButton.click();
|
|
await this.enrichmentPage.waitFor({ state: 'visible', timeout: 15000 });
|
|
testLogger.info('Add Enrichment Table form opened');
|
|
}
|
|
|
|
async fillEnrichmentName(name) {
|
|
testLogger.info(`Filling enrichment table name: ${name}`);
|
|
await this.enrichmentNameField.waitFor({ state: 'visible' });
|
|
await this.enrichmentNameField.fill(name);
|
|
}
|
|
|
|
async clickEnrichmentSave() {
|
|
testLogger.info('Clicking enrichment table Save button');
|
|
await this.enrichmentSaveButton.waitFor({ state: 'visible' });
|
|
await this.enrichmentSaveButton.click();
|
|
}
|
|
|
|
async expectEnrichmentNameError(message) {
|
|
testLogger.info(`Expecting enrichment name error: ${message}`);
|
|
await this.enrichmentNameError.waitFor({ state: 'visible', timeout: 5000 });
|
|
await expect(this.enrichmentNameError).toBeVisible();
|
|
if (message) {
|
|
await expect(this.enrichmentNameError).toContainText(message);
|
|
}
|
|
}
|
|
|
|
async expectEnrichmentFileError(message) {
|
|
testLogger.info(`Expecting enrichment file error: ${message}`);
|
|
await this.enrichmentFileError.waitFor({ state: 'visible', timeout: 5000 });
|
|
await expect(this.enrichmentFileError).toBeVisible();
|
|
if (message) {
|
|
await expect(this.enrichmentFileError).toContainText(message);
|
|
}
|
|
}
|
|
|
|
// ==================== FunctionsToolbar assertion helpers ====================
|
|
|
|
async assertVrlRadioSelected() {
|
|
testLogger.info('Asserting VRL radio is checked');
|
|
await this.vrlRadio.waitFor({ state: 'visible', timeout: 10000 });
|
|
// ORadio renders a native input — check aria-checked or checked attribute
|
|
const checked = await this.vrlRadio.evaluate(el => {
|
|
const input = el.querySelector('input[type="radio"]') || el.closest('[role="radio"]') || el;
|
|
return input.checked || el.getAttribute('aria-checked') === 'true' || el.getAttribute('data-state') === 'checked';
|
|
});
|
|
expect(checked).toBe(true);
|
|
}
|
|
|
|
// ==================== StreamRouting actions ====================
|
|
|
|
async openStreamRoutingForm(org) {
|
|
const targetOrg = org || process.env.ORGID;
|
|
testLogger.info('Opening StreamRouting form via pipeline page');
|
|
await this.page.goto(
|
|
`${process.env.ZO_BASE_URL}/web/pipeline/functions?org_identifier=${targetOrg}`
|
|
);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
// Attempt to open routing panel; fall back gracefully if not available in env
|
|
const routingBtn = this.addStreamRoutingButton;
|
|
const btnVisible = await routingBtn.isVisible({ timeout: 5000 }).catch(() => false);
|
|
if (btnVisible) {
|
|
await routingBtn.click();
|
|
await this.streamRoutingSection.waitFor({ state: 'visible', timeout: 10000 });
|
|
testLogger.info('StreamRouting panel opened via button');
|
|
} else {
|
|
// Navigate directly to routing view if the button is not available
|
|
await this.page.goto(
|
|
`${process.env.ZO_BASE_URL}/web/pipeline/stream-routing?org_identifier=${targetOrg}`
|
|
);
|
|
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
await this.streamRoutingSection.waitFor({ state: 'visible', timeout: 15000 });
|
|
testLogger.info('StreamRouting panel opened via direct URL');
|
|
}
|
|
}
|
|
|
|
async fillStreamRoutingDestName(name) {
|
|
testLogger.info(`Filling stream routing destination name: ${name}`);
|
|
await this.streamRoutingDestNameField.waitFor({ state: 'visible', timeout: 10000 });
|
|
await this.streamRoutingDestNameField.fill(name);
|
|
await this.streamRoutingDestNameField.dispatchEvent('blur');
|
|
}
|
|
|
|
async clickStreamRoutingSave() {
|
|
testLogger.info('Clicking StreamRouting Save button');
|
|
await this.streamRoutingSaveBtn.waitFor({ state: 'visible' });
|
|
await this.streamRoutingSaveBtn.click();
|
|
}
|
|
|
|
async clickStreamRoutingClose() {
|
|
testLogger.info('Clicking StreamRouting close icon');
|
|
await this.streamRoutingCloseBtn.waitFor({ state: 'visible' });
|
|
await this.streamRoutingCloseBtn.click();
|
|
}
|
|
|
|
async assertStreamRoutingNameErrorVisible() {
|
|
testLogger.info('Asserting stream routing destination name error visible');
|
|
await expect(this.streamRoutingDestNameError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async assertStreamRoutingStreamTypeErrorVisible() {
|
|
testLogger.info('Asserting stream routing stream type error visible');
|
|
await expect(this.streamRoutingStreamTypeError).toBeVisible({ timeout: 5000 });
|
|
}
|
|
|
|
async selectUrlSource() {
|
|
testLogger.info('Selecting URL data source');
|
|
// OOptionGroup renders each option — click the "url" option
|
|
const urlOption = this.enrichmentSourceGroup.locator('[data-value="url"], [value="url"]').first();
|
|
// Fallback: click by visible text
|
|
const urlBtn = this.enrichmentSourceGroup.getByText('From URL').first();
|
|
await urlBtn.waitFor({ state: 'visible', timeout: 5000 }).catch(async () => {
|
|
await urlOption.click();
|
|
});
|
|
await urlBtn.click().catch(() => urlOption.click());
|
|
testLogger.info('URL source selected');
|
|
}
|
|
}
|
|
|
|
module.exports = FunctionsFormValidationPage;
|