Files
1adf3c5d11 fix: alert clone, composite Add and dropdown search scroll; deflake alpha1 specs (#15497)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-10-24 07:27:23 +00:00

209 lines
7.6 KiB
JavaScript

/**
* MonacoEditorHelper - Utility class for Monaco editor interactions
*
* Encapsulates Monaco-specific selectors and interactions to:
* 1. Keep spec files clean (POM compliance)
* 2. Centralize Monaco selector changes if Monaco version updates
* 3. Provide consistent interaction patterns across tests
*
* Note: Monaco editor is a third-party component. These selectors
* may need updates when Monaco version changes.
*/
const { expect } = require('@playwright/test');
class MonacoEditorHelper {
/**
* @param {import('@playwright/test').Page} page - Playwright page object
*/
constructor(page) {
this.page = page;
// Monaco editor selectors (third-party component)
this.selectors = {
editor: '.monaco-editor',
viewLines: '.monaco-editor .view-lines',
inputArea: '.monaco-editor textarea.inputarea',
suggestWidget: '.monaco-editor .suggest-widget',
suggestionRows: '.monaco-editor .suggest-widget .monaco-list-row',
suggestionLabel: '.label-name, .monaco-icon-label-container',
};
}
/**
* Get the Monaco editor within a container
* @param {import('@playwright/test').Locator} container - Container locator with data-test attribute
* @returns {import('@playwright/test').Locator}
*/
getEditor(container) {
return container.locator(this.selectors.viewLines).first();
}
/**
* Get the input area (textarea) within a container
* @param {import('@playwright/test').Locator} container - Container locator
* @returns {import('@playwright/test').Locator}
*/
getInputArea(container) {
return container.locator(this.selectors.inputArea);
}
/**
* Click into the Monaco editor to focus it
* @param {import('@playwright/test').Locator} container - Container locator
*/
async focus(container) {
const inputArea = this.getInputArea(container);
// The textarea lags the container when the editor is revealed via v-if; wait for it to attach so a one-shot count() can't miss it and skip focusing (→ typed text lands nowhere).
await inputArea.waitFor({ state: 'attached', timeout: 10000 }).catch(() => {});
if (await inputArea.count() > 0) {
await inputArea.focus();
} else {
const editor = this.getEditor(container);
await editor.click({ force: true });
}
}
/**
* Clear all content from the Monaco editor
* @param {import('@playwright/test').Locator} container - Container locator
*/
async clear(container) {
await this.focus(container);
// ControlOrMeta, not Control: on macOS Control+a is "move to line start",
// so a plain Control+a leaves the editor untouched and the next type()
// appends to the existing query instead of replacing it.
await this.page.keyboard.press('ControlOrMeta+a');
await this.page.keyboard.press('Backspace');
}
/**
* Type text into the Monaco editor
* @param {import('@playwright/test').Locator} container - Container locator
* @param {string} text - Text to type
*/
async type(container, text) {
await this.focus(container);
await this.page.keyboard.type(text);
}
/**
* Set content in the Monaco editor (clears first, then types)
* @param {import('@playwright/test').Locator} container - Container locator
* @param {string} content - Content to set
*/
async setContent(container, content) {
await this.clear(container);
await this.page.keyboard.type(content);
}
/**
* Set content and confirm the editor model holds exactly it, retyping if the text did not replace what was there.
* @param {import('@playwright/test').Locator} container - Container locator
* @param {string} content - Content to set
* @param {number} timeout - Timeout in milliseconds (default 15000)
*/
async setContentVerified(container, content, timeout = 15000) {
const editorCall = (action) => container.evaluate((node, act) => {
const editor = window.monaco?.editor?.getEditors?.().find((e) => node.contains(e.getDomNode()));
if (!editor) return null;
if (act === 'select-all') {
editor.focus();
editor.setSelection(editor.getModel().getFullModelRange());
}
return editor.getValue();
}, action);
// The keyboard select-all can miss the editor's input, so the selection goes through the editor; the text is still typed.
await expect.poll(async () => {
if ((await editorCall('read')) === content) return true;
await editorCall('select-all');
await this.page.keyboard.press('Backspace');
await this.page.keyboard.type(content);
return (await editorCall('read')) === content;
}, { timeout, intervals: [500, 1000, 2000] }).toBe(true);
}
/**
* Get the text content of the Monaco editor
* @param {import('@playwright/test').Locator} container - Container locator
* @returns {Promise<string>}
*/
async getContent(container) {
const viewLines = container.locator(this.selectors.viewLines);
return await viewLines.textContent();
}
/**
* Trigger autocomplete suggestions (Ctrl+Space)
*/
async triggerSuggestions() {
await this.page.keyboard.press('Control+Space');
}
/**
* Wait for the suggestions widget to be visible
* @param {number} timeout - Timeout in milliseconds (default 5000)
* @returns {Promise<import('@playwright/test').Locator>}
*/
async waitForSuggestions(timeout = 5000) {
const suggestWidget = this.page.locator(this.selectors.suggestWidget);
await suggestWidget.waitFor({ state: 'visible', timeout });
return suggestWidget;
}
/**
* Check if suggestions widget is visible
* @returns {Promise<boolean>}
*/
async isSuggestionsVisible() {
const suggestWidget = this.page.locator(this.selectors.suggestWidget);
return await suggestWidget.isVisible();
}
/**
* Get all suggestion labels from the autocomplete widget
* @param {number} timeout - Timeout to wait for suggestions (default 5000)
* @returns {Promise<string[]>}
*/
async getSuggestionLabels(timeout = 5000) {
const suggestionRows = this.page.locator(this.selectors.suggestionRows);
await suggestionRows.first().waitFor({ state: 'visible', timeout });
const labels = await suggestionRows
.locator(this.selectors.suggestionLabel)
.allTextContents();
return labels.map(l => l.trim()).filter(l => l.length > 0);
}
/**
* Check if suggestions contain a specific value
* @param {string} value - Value to search for
* @returns {Promise<boolean>}
*/
async suggestionsContain(value) {
const labels = await this.getSuggestionLabels();
return labels.some(label => label.includes(value));
}
/**
* Select a suggestion by text
* @param {string} text - Text of the suggestion to select
*/
async selectSuggestion(text) {
const suggestionRow = this.page.locator(
`${this.selectors.suggestionRows}:has-text("${text}")`
).first();
await suggestionRow.click();
}
/**
* Dismiss the suggestions widget by clicking outside the editor
*/
async dismissSuggestions() {
await this.page.locator('body').click({ position: { x: 10, y: 10 } });
}
}
module.exports = MonacoEditorHelper;