mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
293 lines
12 KiB
JavaScript
293 lines
12 KiB
JavaScript
// Copyright 2026 OpenObserve Inc.
|
|
|
|
import { expect } from '@playwright/test';
|
|
const testLogger = require('../../playwright-tests/utils/test-logger.js');
|
|
const { getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js');
|
|
|
|
/**
|
|
* AlertSaveQueryModePage — the "Choose Query Mode" dialog Save opens when more
|
|
* than one query mode is in play, plus the bits of the alert editor those tests
|
|
* drive (the mode toggle, the SQL editor, the stream/destination selects).
|
|
*
|
|
* Owns the alert-save-mode-* selectors so specs stay selector-free.
|
|
*/
|
|
export class AlertSaveQueryModePage {
|
|
constructor(page) {
|
|
this.page = page;
|
|
this.locators = {
|
|
// ── the dialog ──────────────────────────────────────────────────────
|
|
dialog: '[data-test="alert-save-mode-dialog"]',
|
|
lead: '[data-test="alert-save-mode-lead"]',
|
|
warning: '[data-test="alert-save-mode-warning"]',
|
|
note: '[data-test="alert-save-mode-note"]',
|
|
primaryBtn: '[data-test="alert-save-mode-dialog"] [data-test="o-dialog-primary-btn"]',
|
|
secondaryBtn: '[data-test="alert-save-mode-dialog"] [data-test="o-dialog-secondary-btn"]',
|
|
|
|
// ── the editor ──────────────────────────────────────────────────────
|
|
submit: '[data-test="add-alert-submit-btn"]',
|
|
nameTrigger: '[data-test="add-alert-name-input-trigger"]',
|
|
nameInput: '[data-test="add-alert-name-input-input"]',
|
|
streamTrigger: '[data-test="add-alert-stream-name-select-dropdown-trigger"]',
|
|
destinationTrigger: '[data-test="alert-destinations-select-trigger"]',
|
|
addCondition: '[data-test="alert-conditions-add-condition-btn"]',
|
|
conditionColumn: '[data-test="alert-conditions-select-column-trigger"]',
|
|
conditionValue: '[data-test="alert-conditions-value-input-field"]',
|
|
threshold: '[data-test="alert-trigger-threshold-input-field"]',
|
|
realtimeTab: '[data-test="add-alert-type-tab-true"]',
|
|
scheduledTab: '[data-test="add-alert-type-tab-false"]',
|
|
alertRulesTab: '[data-test="add-alert-tab-condition"]',
|
|
advancedTab: '[data-test="add-alert-tab-advanced"]',
|
|
streamTypeTrigger: '[data-test="add-alert-stream-type-select-dropdown-trigger"]',
|
|
// Compare-with-Past lives on the Advanced tab, not Alert Rules.
|
|
multiWindowAdd: '[data-test="multi-time-range-alerts-add-btn"]',
|
|
multiWindowDelete: '[data-test="multi-time-range-alerts-delete-btn"]',
|
|
modeToggle: '[data-test="step2-query-tabs"]',
|
|
// Two nodes carry this: the visible query editor and the hidden anomaly preview.
|
|
queryEditor: '[data-test="query-editor"]:visible',
|
|
detailCondition: '[data-test="alerts-alertconfigsummary-condition"]',
|
|
};
|
|
}
|
|
|
|
modeOption(mode) {
|
|
return `[data-test="alert-save-mode-option-${mode}"]`;
|
|
}
|
|
|
|
modeTab(mode) {
|
|
return `[data-test="query-mode-${mode}"]`;
|
|
}
|
|
|
|
// ── navigation ─────────────────────────────────────────────────────────
|
|
|
|
async openEditor(alertId, { folder = 'default' } = {}) {
|
|
await this.page.goto(`/web/alerts?org_identifier=${getOrgIdentifier()}&action=update&alert_id=${alertId}&folder=${folder}`);
|
|
await expect(this.page.locator(this.locators.submit)).toBeVisible({ timeout: 30000 });
|
|
// The form shell renders before the alert is fetched, and a Save fired in
|
|
// that window reads an unpopulated form. The stream select is empty until
|
|
// the alert lands, so it doubles as the "data is in" signal.
|
|
await expect(this.page.locator(this.locators.streamTrigger))
|
|
.not.toHaveAttribute('data-test-selected-value', '', { timeout: 30000 });
|
|
testLogger.info('Opened alert editor', { alertId });
|
|
}
|
|
|
|
async openNewAlert({ folder = 'default' } = {}) {
|
|
await this.page.goto(`/web/alerts?org_identifier=${getOrgIdentifier()}&action=add&folder=${folder}`);
|
|
await expect(this.page.locator(this.locators.submit)).toBeVisible({ timeout: 30000 });
|
|
}
|
|
|
|
async openDetailConfiguration(alertId, { folder = 'default' } = {}) {
|
|
await this.page.goto(`/web/alerts/detail/${alertId}?org_identifier=${getOrgIdentifier()}&folder=${folder}`);
|
|
await this.page.getByRole('tab', { name: 'Configuration' }).click();
|
|
}
|
|
|
|
// ── form input ─────────────────────────────────────────────────────────
|
|
|
|
/** Reka's listbox renders into a portal, so options are addressed by role. */
|
|
async #pickFromDropdown(trigger, optionName) {
|
|
await this.page.locator(trigger).click();
|
|
const choice = this.page.getByRole('option', { name: optionName, exact: true });
|
|
await choice.waitFor({ state: 'visible', timeout: 15000 });
|
|
await choice.click();
|
|
}
|
|
|
|
async selectStream(streamName) {
|
|
await this.#pickFromDropdown(this.locators.streamTrigger, streamName);
|
|
}
|
|
|
|
/** Multi-select: the popover stays open after a pick, so it needs dismissing. */
|
|
async selectDestination(destinationName) {
|
|
await this.#pickFromDropdown(this.locators.destinationTrigger, destinationName);
|
|
await this.page.keyboard.press('Escape');
|
|
}
|
|
|
|
async addBuilderCondition(column, value) {
|
|
await this.page.locator(this.locators.addCondition).click();
|
|
await this.#pickFromDropdown(this.locators.conditionColumn, column);
|
|
await this.page.locator(this.locators.conditionValue).fill(value);
|
|
}
|
|
|
|
/** The name field is an inline edit: open it, then type. Enter submits the form. */
|
|
async setName(name, { submit = false } = {}) {
|
|
await this.page.locator(this.locators.nameTrigger).click();
|
|
await this.page.locator(this.locators.nameInput).fill(name);
|
|
if (submit) await this.page.locator(this.locators.nameInput).press('Enter');
|
|
}
|
|
|
|
async setThreshold(value) {
|
|
await this.page.locator(this.locators.threshold).fill(value);
|
|
}
|
|
|
|
async selectQueryMode(mode) {
|
|
await this.page.locator(this.modeTab(mode)).click();
|
|
}
|
|
|
|
async selectRealtime() {
|
|
await this.page.locator(this.locators.realtimeTab).click();
|
|
}
|
|
|
|
async selectScheduled() {
|
|
await this.page.locator(this.locators.scheduledTab).click();
|
|
}
|
|
|
|
async selectStreamType(streamType) {
|
|
await this.#pickFromDropdown(this.locators.streamTypeTrigger, streamType);
|
|
}
|
|
|
|
async openAdvancedTab() {
|
|
await this.page.locator(this.locators.advancedTab).click();
|
|
await expect(this.page.locator(this.locators.multiWindowAdd)).toBeVisible({ timeout: 20000 });
|
|
}
|
|
|
|
/** Compare-with-Past reference window. Requires the Advanced tab to be open. */
|
|
async addCompareWithPastWindow() {
|
|
const before = await this.page.locator(this.locators.multiWindowDelete).count();
|
|
await this.page.locator(this.locators.multiWindowAdd).click();
|
|
await expect(this.page.locator(this.locators.multiWindowDelete)).toHaveCount(before + 1, {
|
|
timeout: 20000,
|
|
});
|
|
}
|
|
|
|
async expectCompareWithPastWindowCount(count) {
|
|
await expect(this.page.locator(this.locators.multiWindowDelete)).toHaveCount(count, {
|
|
timeout: 20000,
|
|
});
|
|
}
|
|
|
|
async clickSave() {
|
|
await this.page.locator(this.locators.submit).click();
|
|
}
|
|
|
|
/**
|
|
* Monaco auto-closes quotes, so typing a query key by key turns `FROM "x"`
|
|
* into `FROM "x""` and the parser gives up — which silently kills the stream
|
|
* sync some tests assert on. Filling the backing textarea sets the model in
|
|
* one go and skips the auto-close entirely. A fill that lands before Monaco
|
|
* has wired up its model is dropped without error, so the write is retried
|
|
* until the new FROM table is on screen.
|
|
*/
|
|
async setSql(sql) {
|
|
const editor = this.page.locator(this.locators.queryEditor).first();
|
|
await editor.waitFor({ state: 'visible', timeout: 20000 });
|
|
const input = editor.locator('textarea.inputarea');
|
|
await input.waitFor({ state: 'attached', timeout: 20000 });
|
|
const table = (sql.match(/FROM\s+"([^"]+)"/i) || [])[1];
|
|
await expect(async () => {
|
|
await editor.locator('.view-lines').click();
|
|
await input.fill(sql);
|
|
await expect(editor).toContainText(table, { timeout: 5000 });
|
|
}).toPass({ timeout: 40000 });
|
|
// Click away afterwards: the stream-name sync runs off the committed value,
|
|
// which is why #15091/#15092 both say "click outside the editor and wait".
|
|
await this.page.locator(this.locators.alertRulesTab).click();
|
|
}
|
|
|
|
// ── dialog actions ─────────────────────────────────────────────────────
|
|
|
|
async pickSaveMode(mode) {
|
|
await this.page.locator(this.modeOption(mode)).click();
|
|
await expect(this.page.locator(this.modeOption(mode))).toHaveAttribute('data-state', 'on');
|
|
}
|
|
|
|
/**
|
|
* The dialog closes before the request is sent — `saveWithPickedMode` clears
|
|
* the open flag, then awaits a tick and submits — so a dialog-hidden wait
|
|
* returns while the PUT is still in flight and a caller reading the alert
|
|
* back sees the pre-save copy. The editor closing is the signal that the save
|
|
* actually round-tripped.
|
|
*/
|
|
async confirmSaveMode() {
|
|
await this.page.locator(this.locators.primaryBtn).click();
|
|
await expect(this.page.locator(this.locators.dialog)).toBeHidden({ timeout: 30000 });
|
|
await expect(this.page.locator(this.locators.submit)).toBeHidden({ timeout: 30000 });
|
|
}
|
|
|
|
async cancelSaveMode() {
|
|
await this.page.locator(this.locators.secondaryBtn).click();
|
|
await expect(this.page.locator(this.locators.dialog)).toBeHidden();
|
|
}
|
|
|
|
// ── assertions ─────────────────────────────────────────────────────────
|
|
|
|
async expectDialogOpen() {
|
|
await expect(this.page.locator(this.locators.dialog)).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async expectDialogClosed() {
|
|
await expect(this.page.locator(this.locators.dialog)).toBeHidden();
|
|
}
|
|
|
|
async expectLead(text) {
|
|
await expect(this.page.locator(this.locators.lead)).toHaveText(text);
|
|
}
|
|
|
|
async expectWarning(text) {
|
|
await expect(this.page.locator(this.locators.warning)).toHaveText(text);
|
|
}
|
|
|
|
async expectNoWarning() {
|
|
await expect(this.page.locator(this.locators.warning)).toHaveCount(0);
|
|
}
|
|
|
|
async expectNote(text) {
|
|
await expect(this.page.locator(this.locators.note)).toHaveText(text);
|
|
}
|
|
|
|
async expectPrimaryButtonLabel(text) {
|
|
await expect(this.page.locator(this.locators.primaryBtn)).toHaveText(text);
|
|
}
|
|
|
|
async expectSaveModeSelected(mode) {
|
|
await expect(this.page.locator(this.modeOption(mode))).toHaveAttribute('data-state', 'on');
|
|
}
|
|
|
|
async expectQueryModeSelected(mode) {
|
|
await expect(this.page.locator(this.modeTab(mode))).toHaveAttribute('data-state', 'on');
|
|
}
|
|
|
|
async expectModeToggleHidden() {
|
|
await expect(this.page.locator(this.locators.modeToggle)).toBeHidden();
|
|
}
|
|
|
|
async expectOnEditor() {
|
|
await expect(this.page.locator(this.locators.submit)).toBeVisible();
|
|
}
|
|
|
|
/**
|
|
* The dialog is the only thing between Save and the request, so "no dialog"
|
|
* is asserted by the save completing: the editor closes back to the list.
|
|
*/
|
|
async expectSavedWithoutDialog() {
|
|
await this.expectDialogClosed();
|
|
await expect(this.page.locator(this.locators.submit)).toBeHidden({ timeout: 30000 });
|
|
}
|
|
|
|
async expectStreamName(streamName) {
|
|
await expect(this.page.locator(this.locators.streamTrigger)).toHaveText(streamName, { timeout: 20000 });
|
|
}
|
|
|
|
async expectThreshold(value) {
|
|
// The field repaints from the fetched alert, so allow for the round trip.
|
|
await expect(this.page.locator(this.locators.threshold)).toHaveValue(value, { timeout: 20000 });
|
|
}
|
|
|
|
async expectSqlContains(text) {
|
|
await expect(this.page.locator(this.locators.queryEditor).first())
|
|
.toContainText(text, { timeout: 20000 });
|
|
}
|
|
|
|
async expectValidationToast() {
|
|
await expect(
|
|
this.page.getByText('Please fix the highlighted fields before saving.').first(),
|
|
).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
async expectDetailConditionEmpty() {
|
|
const condition = this.page.locator(this.locators.detailCondition);
|
|
await expect(condition).toContainText('—');
|
|
await expect(condition).not.toContainText('SELECT');
|
|
}
|
|
|
|
async expectDetailConditionShowsSql() {
|
|
await expect(this.page.locator(this.locators.detailCondition)).toContainText('SELECT');
|
|
}
|
|
}
|