Files
openobserve/tests/ui-testing/pages/dashboardPages/dashboardsFormValidationPage.js
T
83d12fff04 feat(web): inline-rename page titles with auto-generated default names (#13520)
## What changed

Naming an alert, panel, workflow, pipeline or function meant inventing a
name up front in a form field before anything else could be configured.
Two pieces replace that.

**Inline rename** — the page title *is* the input. `OInlineEdit` /
`OFormInlineEdit` (`web/src/lib/forms/InlineEdit`): click to rename,
Enter or blur commits, Escape restores the pre-edit value. A `#trail`
slot carries the "Auto" badge in display mode only.

**Auto-generated names** — `utils/autoName.ts` builds the name,
`composables/useAutoName.ts` decides when it's allowed to win. A name is
AUTO until the user types, then it's theirs for good; clearing the field
and blurring re-arms auto. The generators describe only what is actually
configured, so a half-built panel yields a shorter name rather than a
speculative one, and an empty string when there is nothing to say.

- Panel names read as prose — `Avg of duration by service`, `Record
count`, `k8s_logs overview`. Both live panel-field shapes are handled
(builder `functionName` + `args`, and legacy flat `column` +
`aggregationFunction`); the time column is read from
`zoConfig.timestamp_column` rather than hardcoded. Hand-written queries
fall back to the stream, since naming a panel after `x_axis_1` is worse
than saying nothing.
- Alert names are identifiers — `anomaly_k8s_logs`,
`k8s_logs_status_gte_500` — built to survive the backend's
`RE_OFGA_UNSUPPORTED_NAME` rule, with operators spelled out and a
`slugify` backstop. A leading underscore is preserved so internal
streams (`_rumdata`) keep their names.

### Supporting library changes

| Component | Change |
| --- | --- |
| `OPageHeader` / `OPageLayout` | New `titleOverflow="visible"` — the
`<h1>`'s `truncate` clips an interactive title's focus ring and the
error message it floats below itself |
| `OSelect` | New `appearance="inline"` — borderless, no fixed height,
sized to content, for a value that reads as a word inside running text
("Add Alert **in KTX**") |

All new strings go through i18n (`common.inlineEdit.*`,
`dashboard.autoName.*`, `alerts.autoName.*`, per-module `renameHint`).
Interpolated identifiers — column names, SQL functions, stream names —
stay verbatim.

## Wired into

AddPanel, AddAlert (+ `useAlertForm`), WorkflowEditor, PipelineEditor,
Functions / FunctionsToolbar, IncidentDetailDrawer,
InlineSelectFolderDropdown.

## Tested

244 unit tests across the 5 affected suites, all passing — including new
specs for `OInlineEdit`, `useAutoName`, and `autoName`.

One pre-existing expectation in `autoName.spec.ts` was updated: it
asserted `Avg of duration, max of duration +1 more`, capitalising only
the first word of a multi-measure name. `measureLabel` capitalises per
measure by design, so the expectation now reads `Avg of duration, Max of
duration +1 more`.

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
2026-07-31 08:50:19 +00:00

577 lines
27 KiB
JavaScript

// Copyright 2026 OpenObserve Inc.
/**
* DashboardsFormValidationPage
*
* Page Object Model for Dashboard domain form validation:
* - AddDashboard (hosted in dashboard-add-dialog)
* - AddFolder (hosted in dashboard-folder-dialog)
* - AddTab (hosted in dashboard-tab-settings-add-tab-dialog, accessed from within a dashboard)
*
* OInput/OFormInput convention:
* data-test="foo" → native <input> carries data-test="foo-field" (.fill())
* → error span carries data-test="foo-error"
*
* ODialog built-in buttons are scoped to their dialog:
* [data-test="<dialog>"] [data-test="o-dialog-primary-btn"]
* [data-test="<dialog>"] [data-test="o-dialog-secondary-btn"]
*/
export class DashboardsFormValidationPage {
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
this.page = page;
// ── Navigation ────────────────────────────────────────────────────────
this.dashboardsMenuLink = '[data-test="menu-link-/dashboards-item"]';
// ── AddDashboard form ─────────────────────────────────────────────────
// Trigger button on the Dashboards list page
this.newDashboardBtn = '[data-test="dashboard-new"]';
// ODialog wrapper
this.dashboardDialog = '[data-test="dashboard-add-dialog"]';
// OFormInput data-test="add-dashboard-name" → -field / -error
this.dashboardNameInput = '[data-test="add-dashboard-name-field"]';
this.dashboardNameError = '[data-test="add-dashboard-name-error"]';
// ODialog primary/secondary buttons scoped to dialog
this.dashboardSubmitBtn = '[data-test="dashboard-add-dialog"] [data-test="o-dialog-primary-btn"]';
this.dashboardCancelBtn = '[data-test="dashboard-add-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── AddFolder form ────────────────────────────────────────────────────
// Trigger: existing dashboard-folder page object; here we reference the
// new-folder button via the same selector used in dashboard-folder.js.
this.newFolderBtn = '[data-test="dashboard-new-folder-btn"]';
// ODialog wrapper
this.folderDialog = '[data-test="dashboard-folder-dialog"]';
// OFormInput data-test="dashboard-folder-add-name" → -field / -error
this.folderNameInput = '[data-test="dashboard-folder-add-name-field"]';
this.folderNameError = '[data-test="dashboard-folder-add-name-error"]';
// ODialog primary/secondary buttons scoped to dialog
this.folderSubmitBtn = '[data-test="dashboard-folder-dialog"] [data-test="o-dialog-primary-btn"]';
this.folderCancelBtn = '[data-test="dashboard-folder-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── AddTab form ───────────────────────────────────────────────────────
// Trigger: the "+" tab button rendered by TabList.vue inside a dashboard
this.addTabBtn = '[data-test="dashboard-tab-add-btn"]';
// ODialog wrapper (declared directly on AddTab.vue's ODialog)
this.tabDialog = '[data-test="dashboard-tab-settings-add-tab-dialog"]';
// OFormInput data-test="dashboard-add-tab-name" → -field / -error
this.tabNameInput = '[data-test="dashboard-add-tab-name-field"]';
this.tabNameError = '[data-test="dashboard-add-tab-name-error"]';
// ODialog primary/secondary buttons scoped to dialog
this.tabSubmitBtn = '[data-test="dashboard-tab-settings-add-tab-dialog"] [data-test="o-dialog-primary-btn"]';
this.tabCancelBtn = '[data-test="dashboard-tab-settings-add-tab-dialog"] [data-test="o-dialog-secondary-btn"]';
// ── Dashboard list / navigation ───────────────────────────────────────
// Tab-list container rendered once a dashboard is open
this.tabListContainer = '[data-test="dashboard-tab-list-container"]';
// ── Toast ─────────────────────────────────────────────────────────────
this.toastSuccess = '[data-test-variant="success"]';
// ── DrilldownPopup ────────────────────────────────────────────────────
// OFormInput data-test="dashboard-config-panel-drilldown-name" → -field / -error
this.drilldownNameInput = '[data-test="dashboard-config-panel-drilldown-name-field"]';
this.drilldownNameError = '[data-test="dashboard-config-panel-drilldown-name-error"]';
// Add drilldown button (in config panel drilldown section)
this.drilldownAddBtn = '[data-test="dashboard-addpanel-config-drilldown-add-btn"]';
// DrilldownPopUp dialog wrapper
this.drilldownPopup = '[data-test="dashboard-drilldown-popup"]';
// Type selector buttons
this.drilldownByDashboardBtn = '[data-test="dashboard-drilldown-by-dashboard-btn"]';
this.drilldownByUrlBtn = '[data-test="dashboard-drilldown-by-url-btn"]';
this.drilldownByLogsBtn = '[data-test="dashboard-drilldown-by-logs-btn"]';
// URL input and its error
this.drilldownUrlTextarea = '[data-test="dashboard-drilldown-url-textarea"]';
this.drilldownUrlError = '[data-test="dashboard-drilldown-url-error-message"]';
// Dashboard/tab/folder selects (OSelect convention)
this.drilldownFolderSelect = '[data-test="dashboard-drilldown-folder-select-popover"]';
this.drilldownDashboardSelect = '[data-test="dashboard-drilldown-dashboard-select-popover"]';
this.drilldownTabSelect = '[data-test="dashboard-drilldown-tab-select-popover"]';
// ── Dashboard Settings navigation ─────────────────────────────────────
this.dashboardSettingsBtn = '[data-test="dashboard-setting-btn"]';
this.settingsGeneralTab = '[data-test="dashboard-settings-general-tab"]';
this.settingsVariablesTab = '[data-test="dashboard-settings-variable-tab"]';
this.variableAddBtn = '[data-test="dashboard-variable-add-btn"]';
// ── PanelLayoutSettings dialog ────────────────────────────────────────
this.panelLayoutDrawer = '[data-test="panel-layout-settings-drawer"]';
this.panelLayoutHeightField = '[data-test="panel-layout-settings-height-input-field"]';
this.panelLayoutHeightError = '[data-test="panel-layout-settings-height-input-error"]';
this.panelLayoutSaveBtn = '[data-test="panel-layout-settings-drawer"] [data-test="o-dialog-primary-btn"]';
this.panelLayoutCancelBtn = '[data-test="panel-layout-settings-drawer"] [data-test="o-dialog-secondary-btn"]';
// ── AddCondition (panel editor Filters section) ───────────────────────
this.addConditionAddBtn = '[data-test="dashboard-add-condition-add"]';
this.conditionColumn = '[data-test="dashboard-add-condition-column-0"]';
this.conditionCondition = '[data-test="dashboard-add-condition-condition-0"]';
this.conditionRemoveColumn = '[data-test="dashboard-add-condition-remove-column-0"]';
// ── AddAnnotation dialog ──────────────────────────────────────────────
this.addAnnotationDialog = '[data-test="add-annotation-dialog"]';
this.annotationTitleField = '[data-test="dashboard-add-annotation-title-input-field"]';
this.annotationTitleError = '[data-test="dashboard-add-annotation-title-input-error"]';
this.annotationTextField = '[data-test="dashboard-add-annotation-text-input-field"]';
this.annotationPanelsPopover = '[data-test="dashboard-add-annotation-panels-select-popover"]';
// Save/Cancel now use ODialog's built-in footer buttons (scoped to this dialog).
this.annotationCancelBtn = '[data-test="add-annotation-dialog"] [data-test="o-dialog-secondary-btn"]';
this.annotationSaveBtn = '[data-test="add-annotation-dialog"] [data-test="o-dialog-primary-btn"]';
// ── GeneralSettings dialog ────────────────────────────────────────────
this.generalSettingNameField = '[data-test="dashboard-general-setting-name-field"]';
this.generalSettingNameError = '[data-test="dashboard-general-setting-name-error"]';
this.generalSettingDescField = '[data-test="dashboard-general-setting-description-field"]';
this.generalSettingCancelBtn = '[data-test="dashboard-general-setting-cancel-btn"]';
this.generalSettingSaveBtn = '[data-test="dashboard-general-setting-save-btn"]';
// ── Variable Settings (Dashboard Settings > Variables) ────────────────
// OFormInput data-test="dashboard-variable-name" → -field / -error
this.variableNameInput = '[data-test="dashboard-variable-name-field"]';
this.variableNameError = '[data-test="dashboard-variable-name-error"]';
// OSelect data-test="dashboard-variable-type-select" → -popover / -error
this.variableTypeSelectPopover = '[data-test="dashboard-variable-type-select-popover"]';
this.variableTypeError = '[data-test="dashboard-variable-type-select-error"]';
// Action buttons
this.variableCancelBtn = '[data-test="dashboard-variable-cancel-btn"]';
this.variableSaveBtn = '[data-test="dashboard-variable-save-btn"]';
// ── AddCondition (Panel config > Conditions tab) ──────────────────────
// First condition row (index 0)
this.conditionColumn0 = '[data-test="dashboard-add-condition-column-0"]';
this.conditionCondition0 = '[data-test="dashboard-add-condition-condition-0"]';
this.conditionValue = '[data-test="dashboard-add-condition-value"]';
this.conditionRemove = '[data-test="dashboard-add-condition-remove"]';
// ── ConfigPanel (panel editor right sidebar) ──────────────────────────
this.configPanelDescription = '[data-test="dashboard-config-description"]';
this.configPanelYAxisMin = '[data-test="dashboard-config-y_axis_min"]';
this.configPanelYAxisMax = '[data-test="dashboard-config-y_axis_max"]';
this.configPanelDecimals = '[data-test="dashboard-config-decimals"]';
this.configPanelDecimalsField = '[data-test="dashboard-config-decimals-field"]';
this.configPanelDecimalsError = '[data-test="dashboard-config-decimals-error"]';
this.configPanelLimit = '[data-test="dashboard-config-limit"]';
// ── BuildFieldPopUp ───────────────────────────────────────────────────
this.buildFieldPopupContainer = '[data-test="dashboard-build-field-popup-container"]';
this.buildFieldLabelInput = '[data-test="dashboard-x-item-input-field"]';
// Y-axis field chip: data-test="dashboard-y-item-${alias}" where alias = y_axis_1 for the first added field
this.yAxisFieldChipFirst = '[data-test="dashboard-y-item-y_axis_1"]';
// ── AddPanel / PanelEditor toolbar ───────────────────────────────────
// Panel name is an inline-edited title (OFormInlineEdit): -trigger opens
// the editor, -input is the revealed field, -error is the validation msg.
this.panelNameTrigger = '[data-test="dashboard-panel-name-trigger"]';
this.panelNameField = '[data-test="dashboard-panel-name-input"]';
this.panelNameError = '[data-test="dashboard-panel-name-error"]';
this.panelSaveBtn = '[data-test="dashboard-panel-save"]';
this.panelDiscardBtn = '[data-test="dashboard-panel-discard"]';
}
// ── Navigation ────────────────────────────────────────────────────────────
async navigateToDashboards() {
await this.page.locator(this.dashboardsMenuLink).click();
await this.page.locator('[data-test="dashboard-table"]').waitFor({ state: 'visible', timeout: 15000 });
}
// ── AddDashboard helpers ──────────────────────────────────────────────────
async openAddDashboardForm() {
await this.page.locator(this.newDashboardBtn).click();
await this.page.locator(this.dashboardDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillDashboardName(name) {
await this.page.locator(this.dashboardNameInput).fill(name);
}
async submitDashboardForm() {
await this.page.locator(this.dashboardSubmitBtn).click();
}
async cancelDashboardForm() {
await this.page.locator(this.dashboardCancelBtn).click();
}
getDashboardDialogLocator() {
return this.page.locator(this.dashboardDialog);
}
getDashboardNameInputLocator() {
return this.page.locator(this.dashboardNameInput);
}
getDashboardNameErrorLocator() {
return this.page.locator(this.dashboardNameError);
}
getDashboardSubmitBtnLocator() {
return this.page.locator(this.dashboardSubmitBtn);
}
// ── AddFolder helpers ─────────────────────────────────────────────────────
async openAddFolderForm() {
await this.page.locator(this.newFolderBtn).click();
await this.page.locator(this.folderDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillFolderName(name) {
await this.page.locator(this.folderNameInput).fill(name);
}
async submitFolderForm() {
await this.page.locator(this.folderSubmitBtn).click();
}
async cancelFolderForm() {
await this.page.locator(this.folderCancelBtn).click();
}
getFolderDialogLocator() {
return this.page.locator(this.folderDialog);
}
getFolderNameErrorLocator() {
return this.page.locator(this.folderNameError);
}
getFolderSubmitBtnLocator() {
return this.page.locator(this.folderSubmitBtn);
}
// ── AddTab helpers ────────────────────────────────────────────────────────
async openAddTabForm() {
await this.page.locator(this.addTabBtn).click();
await this.page.locator(this.tabDialog).waitFor({ state: 'visible', timeout: 8000 });
}
async fillTabName(name) {
await this.page.locator(this.tabNameInput).fill(name);
}
async submitTabForm() {
await this.page.locator(this.tabSubmitBtn).click();
}
async cancelTabForm() {
await this.page.locator(this.tabCancelBtn).click();
}
getTabDialogLocator() {
return this.page.locator(this.tabDialog);
}
getTabNameErrorLocator() {
return this.page.locator(this.tabNameError);
}
getTabSubmitBtnLocator() {
return this.page.locator(this.tabSubmitBtn);
}
// ── Dashboard list navigation helpers ────────────────────────────────────
/**
* Returns a locator for a dashboard row identified by its data-test-dashboard-name attribute.
* @param {string} dashName
*/
getDashboardByNameLocator(dashName) {
return this.page.locator(`[data-test="dashboard-name-cell-${dashName}"]`).first();
}
/**
* Clicks into the dashboard row for dashName.
* @param {string} dashName
*/
async openDashboardByName(dashName) {
await this.page.locator(`[data-test="dashboard-name-cell-${dashName}"]`).first().click();
}
/**
* Waits for the tab-list container to be visible after opening a dashboard.
* @param {number} [timeout=15000]
*/
async waitForTabListContainer(timeout = 15000) {
await this.page.locator(this.tabListContainer).waitFor({ state: 'visible', timeout });
}
getTabListContainerLocator() {
return this.page.locator(this.tabListContainer);
}
// ── DrilldownPopup helpers ────────────────────────────────────────────────
async clickDrilldownByUrlBtn() {
await this.page.locator(this.drilldownByUrlBtn).click();
}
async fillDrilldownUrl(url) {
await this.page.locator(this.drilldownUrlTextarea).fill(url);
}
getDrilldownNameInputLocator() {
return this.page.locator(this.drilldownNameInput);
}
getDrilldownNameErrorLocator() {
return this.page.locator(this.drilldownNameError);
}
getDrilldownAddBtnLocator() {
return this.page.locator(this.drilldownAddBtn);
}
getDrilldownPopupLocator() {
return this.page.locator(this.drilldownPopup);
}
getDrilldownByDashboardBtnLocator() {
return this.page.locator(this.drilldownByDashboardBtn);
}
getDrilldownByUrlBtnLocator() {
return this.page.locator(this.drilldownByUrlBtn);
}
getDrilldownByLogsBtnLocator() {
return this.page.locator(this.drilldownByLogsBtn);
}
getDrilldownUrlTextareaLocator() {
return this.page.locator(this.drilldownUrlTextarea);
}
getDrilldownUrlErrorLocator() {
return this.page.locator(this.drilldownUrlError);
}
// ── Variable Settings helpers ─────────────────────────────────────────────
async fillVariableName(name) {
await this.page.locator(this.variableNameInput).fill(name);
}
async openVariableTypeSelect() {
await this.page.locator(this.variableTypeSelectPopover).click();
}
async clickVariableSave() {
await this.page.locator(this.variableSaveBtn).click();
}
async clickVariableCancel() {
await this.page.locator(this.variableCancelBtn).click();
}
getDashboardSettingsBtnLocator() {
return this.page.locator(this.dashboardSettingsBtn);
}
getSettingsGeneralTabLocator() {
return this.page.locator(this.settingsGeneralTab);
}
getSettingsVariablesTabLocator() {
return this.page.locator(this.settingsVariablesTab);
}
getVariableAddBtnLocator() {
return this.page.locator(this.variableAddBtn);
}
getVariableNameInputLocator() {
return this.page.locator(this.variableNameInput);
}
getVariableNameErrorLocator() {
return this.page.locator(this.variableNameError);
}
getVariableTypeSelectPopoverLocator() {
return this.page.locator(this.variableTypeSelectPopover);
}
getVariableTypeErrorLocator() {
return this.page.locator(this.variableTypeError);
}
getVariableCancelBtnLocator() {
return this.page.locator(this.variableCancelBtn);
}
getVariableSaveBtnLocator() {
return this.page.locator(this.variableSaveBtn);
}
// ── PanelLayoutSettings helpers ───────────────────────────────────────────
getPanelLayoutDrawerLocator() {
return this.page.locator(this.panelLayoutDrawer);
}
getPanelLayoutHeightFieldLocator() {
return this.page.locator(this.panelLayoutHeightField);
}
getPanelLayoutHeightErrorLocator() {
return this.page.locator(this.panelLayoutHeightError);
}
getPanelLayoutSaveBtnLocator() {
return this.page.locator(this.panelLayoutSaveBtn);
}
getPanelLayoutCancelBtnLocator() {
return this.page.locator(this.panelLayoutCancelBtn);
}
// ── AddCondition helpers ──────────────────────────────────────────────────
getAddConditionAddBtnLocator() {
return this.page.locator(this.addConditionAddBtn);
}
getConditionColumnLocator() {
return this.page.locator(this.conditionColumn);
}
getConditionConditionLocator() {
return this.page.locator(this.conditionCondition);
}
getConditionRemoveColumnLocator() {
return this.page.locator(this.conditionRemoveColumn);
}
// ── Dashboard list search (used to detect dashboards list is ready) ──────
getDashboardSearchLocator() {
return this.page.locator('[data-test="dashboard-search"]');
}
// ── Annotation button (on panel in view mode) ─────────────────────────────
getAnnotationModeButtonLocator() {
return this.page.locator('[data-test="panel-schema-renderer-annotation-button"]');
}
getPanelCanvasLocator() {
return this.page.locator('[data-test="dashboard-panel-bar"]').first().locator('canvas').first();
}
// ── AddAnnotation helpers ─────────────────────────────────────────────────
getAddAnnotationDialogLocator() {
return this.page.locator(this.addAnnotationDialog);
}
getAnnotationTitleFieldLocator() {
return this.page.locator(this.annotationTitleField);
}
getAnnotationTitleErrorLocator() {
return this.page.locator(this.annotationTitleError);
}
getAnnotationTextFieldLocator() {
return this.page.locator(this.annotationTextField);
}
getAnnotationPanelsPopoverLocator() {
return this.page.locator(this.annotationPanelsPopover);
}
getAnnotationCancelBtnLocator() {
return this.page.locator(this.annotationCancelBtn);
}
getAnnotationSaveBtnLocator() {
return this.page.locator(this.annotationSaveBtn);
}
// ── GeneralSettings helpers ───────────────────────────────────────────────
getGeneralSettingNameFieldLocator() {
return this.page.locator(this.generalSettingNameField);
}
getGeneralSettingNameErrorLocator() {
return this.page.locator(this.generalSettingNameError);
}
getGeneralSettingDescFieldLocator() {
return this.page.locator(this.generalSettingDescField);
}
getGeneralSettingCancelBtnLocator() {
return this.page.locator(this.generalSettingCancelBtn);
}
getGeneralSettingSaveBtnLocator() {
return this.page.locator(this.generalSettingSaveBtn);
}
// ── AddCondition helpers ──────────────────────────────────────────────────
getConditionColumn0Locator() {
return this.page.locator(this.conditionColumn0);
}
getConditionCondition0Locator() {
return this.page.locator(this.conditionCondition0);
}
getConditionValueLocator() {
return this.page.locator(this.conditionValue);
}
getConditionRemoveLocator() {
return this.page.locator(this.conditionRemove);
}
// ── ConfigPanel helpers ───────────────────────────────────────────────────
getConfigPanelDescriptionLocator() { return this.page.locator(this.configPanelDescription); }
getConfigPanelYAxisMinLocator() { return this.page.locator(this.configPanelYAxisMin); }
getConfigPanelYAxisMaxLocator() { return this.page.locator(this.configPanelYAxisMax); }
getConfigPanelDecimalsLocator() { return this.page.locator(this.configPanelDecimals); }
getConfigPanelDecimalsFieldLocator() { return this.page.locator(this.configPanelDecimalsField); }
getConfigPanelDecimalsErrorLocator() { return this.page.locator(this.configPanelDecimalsError); }
getConfigPanelLimitLocator() { return this.page.locator(this.configPanelLimit); }
// ── BuildFieldPopUp helpers ───────────────────────────────────────────────
getBuildFieldPopupContainerLocator(){ return this.page.locator(this.buildFieldPopupContainer); }
getBuildFieldLabelInputLocator() { return this.page.locator(this.buildFieldLabelInput); }
getYAxisFieldChipFirstLocator() { return this.page.locator(this.yAxisFieldChipFirst); }
// ── AddPanel / PanelEditor helpers ────────────────────────────────────────
// The name display trigger (always rendered) — use it as the "editor opened"
// anchor. The `-input` only exists while editing, so it can't anchor setup.
getPanelNameTriggerLocator() { return this.page.locator(this.panelNameTrigger); }
getPanelNameFieldLocator() { return this.page.locator(this.panelNameField); }
getPanelNameErrorLocator() { return this.page.locator(this.panelNameError); }
getPanelSaveBtnLocator() { return this.page.locator(this.panelSaveBtn); }
getPanelDiscardBtnLocator() { return this.page.locator(this.panelDiscardBtn); }
async fillPanelName(name) {
// Open the inline editor via its trigger, then fill the revealed input.
await this.page.locator(this.panelNameTrigger).click();
const field = this.page.locator(this.panelNameField);
await field.waitFor({ state: 'visible', timeout: 10000 });
await field.fill(name);
}
async clearPanelName() {
await this.page.locator(this.panelNameTrigger).click();
const field = this.page.locator(this.panelNameField);
await field.waitFor({ state: 'visible', timeout: 10000 });
await field.fill('');
}
}