mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
test: add E2E tests for autocomplete value suggestions feature (#11027)
Add Playwright E2E tests for the new autocomplete value suggestions feature that persists field values in IndexedDB and shows them when typing `field =`. Tests cover: - IndexedDB capture from field expansion (Values API) - IndexedDB capture from search results (marked as expected-to-fail due to bug) - Autocomplete suggestions for operators: =, !=, <>, >=, <=, IN, NOT IN, LIKE - Numeric value quoting (no quotes for numbers) - TTL expiry filtering - Stream/org isolation - Traces page autocomplete Note: One test marked as expected-to-fail due to BLOCKER BUG where captureFromSearchHits() doesn't write to IndexedDB (schemaFields filtering issue) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Neha P <undefined@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude
claude[bot]
Neha P
parent
f8b2e5e9fa
commit
258eecd447
@@ -157,6 +157,7 @@ jobs:
|
||||
"logsqueries.cte.spec.js",
|
||||
"logsqueries.matchall.spec.js",
|
||||
"searchpartition.spec.js",
|
||||
"logs-autocomplete-suggestions.spec.js",
|
||||
]
|
||||
- testfolder: "Alerts"
|
||||
browser: "chrome"
|
||||
@@ -262,6 +263,7 @@ jobs:
|
||||
"traceDetails.spec.js",
|
||||
"traceAdvancedFiltering.spec.js",
|
||||
"tracesAnalyzeDimensions.spec.js",
|
||||
"traces-autocomplete-suggestions.spec.js",
|
||||
]
|
||||
|
||||
- testfolder: "RegressionSet"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* Traces Autocomplete Value Suggestions E2E Tests
|
||||
*
|
||||
* Tests the autosuggestions feature on the Traces page.
|
||||
* The feature captures field values when expanding fields in the sidebar
|
||||
* and shows them as autocomplete suggestions when typing `field =`.
|
||||
*
|
||||
* Based on: docs/impact-reports/autosuggestions-feature-impact.md
|
||||
*/
|
||||
|
||||
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
|
||||
const testLogger = require('../utils/test-logger.js');
|
||||
const PageManager = require('../../pages/page-manager.js');
|
||||
const MonacoEditorHelper = require('../utils/MonacoEditorHelper.js');
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
async function clearIndexedDB(page) {
|
||||
await page.evaluate(async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.deleteDatabase('o2FieldValues');
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onblocked = () => resolve(); // Blocked is not an error, just a delay
|
||||
});
|
||||
});
|
||||
testLogger.info('Cleared IndexedDB o2FieldValues database');
|
||||
}
|
||||
|
||||
async function getIndexedDBRecords(page) {
|
||||
return await page.evaluate(async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open('o2FieldValues', 1);
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onsuccess = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('fieldValues')) {
|
||||
db.close();
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
const tx = db.transaction('fieldValues', 'readonly');
|
||||
const store = tx.objectStore('fieldValues');
|
||||
const getAllReq = store.getAll();
|
||||
getAllReq.onsuccess = () => {
|
||||
db.close();
|
||||
resolve(getAllReq.result || []);
|
||||
};
|
||||
getAllReq.onerror = () => {
|
||||
db.close();
|
||||
reject(getAllReq.error);
|
||||
};
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MonacoEditorHelper instance for traces
|
||||
*/
|
||||
function getMonacoHelper(page) {
|
||||
return new MonacoEditorHelper(page);
|
||||
}
|
||||
|
||||
async function waitForSuggestionsWidget(page, timeout = 5000) {
|
||||
const monacoHelper = getMonacoHelper(page);
|
||||
return await monacoHelper.waitForSuggestions(timeout);
|
||||
}
|
||||
|
||||
async function getSuggestionLabels(page) {
|
||||
const monacoHelper = getMonacoHelper(page);
|
||||
return await monacoHelper.getSuggestionLabels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to expand a field using multiple strategies
|
||||
* Follows the pattern from traceQueryEditor.spec.js
|
||||
* Waits for field values API response after expanding
|
||||
*/
|
||||
async function tryExpandField(page, pm) {
|
||||
const orgName = process.env["ORGNAME"] || 'default';
|
||||
|
||||
// Strategy 1: Try common trace field names via tracesPage method
|
||||
// These match the patterns used in traceQueryEditor.spec.js
|
||||
const traceFieldNames = [
|
||||
'status_code', // Common in trace data
|
||||
'service_name', // Common trace attribute
|
||||
'http.status_code', // OpenTelemetry semantic convention
|
||||
'http.method', // OpenTelemetry semantic convention
|
||||
'service.name', // OpenTelemetry semantic convention
|
||||
'span.kind', // OpenTelemetry attribute
|
||||
'environment', // Custom attribute
|
||||
'operation_name' // Jaeger-style attribute
|
||||
];
|
||||
|
||||
for (const fieldName of traceFieldNames) {
|
||||
// Start listening for values API response before clicking
|
||||
const valuesResponsePromise = page.waitForResponse(
|
||||
response => response.url().includes(`/api/${orgName}/`) && response.url().includes('_values'),
|
||||
{ timeout: 10000 }
|
||||
).catch(() => null);
|
||||
|
||||
const expanded = await pm.tracesPage.expandTraceField(fieldName);
|
||||
if (expanded) {
|
||||
testLogger.info(`Expanded trace field: ${fieldName}`);
|
||||
|
||||
// Wait for values API response
|
||||
const response = await valuesResponsePromise;
|
||||
if (response) {
|
||||
testLogger.info(`Values API responded for field: ${fieldName}`);
|
||||
// Give IndexedDB time to write
|
||||
await page.waitForTimeout(2000);
|
||||
} else {
|
||||
testLogger.info(`No values API response captured for field: ${fieldName}`);
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
return { fieldName, success: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Click on field expansion headers in the traces field list
|
||||
// Traces uses q-expansion-item with class 'field-expansion-item'
|
||||
// The clickable header has class 'field-expansion-header'
|
||||
// Using page object selector for the fields table
|
||||
const fieldsTableSelector = pm.tracesPage.fieldsTable || '[data-test="log-search-index-list-fields-table"]';
|
||||
const fieldsTable = page.locator(fieldsTableSelector);
|
||||
if (await fieldsTable.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
// Look for field expansion items directly on the page (they're inside the table)
|
||||
// Note: .field-expansion-item is a Quasar component class, not a data-test selector
|
||||
const fieldHeaders = page.locator('.field-expansion-item .field-expansion-header');
|
||||
const headerCount = await fieldHeaders.count();
|
||||
testLogger.info(`Found ${headerCount} field expansion headers in traces field list`);
|
||||
|
||||
if (headerCount > 0) {
|
||||
// Try first few fields (skip internal fields like _timestamp)
|
||||
for (let i = 0; i < Math.min(headerCount, 10); i++) {
|
||||
const header = fieldHeaders.nth(i);
|
||||
const headerText = await header.textContent().catch(() => '');
|
||||
|
||||
// Skip system fields
|
||||
if (headerText.includes('_timestamp') || headerText.includes('_id') || headerText.includes('duration')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract field name from the header
|
||||
const fieldName = headerText.trim().split(/\s+/)[0] || `field_${i}`;
|
||||
testLogger.info(`Attempting to expand field: ${fieldName}`);
|
||||
|
||||
// Start listening for values API before clicking
|
||||
const valuesResponsePromise = page.waitForResponse(
|
||||
response => response.url().includes(`/api/${orgName}/`) && response.url().includes('_values'),
|
||||
{ timeout: 15000 }
|
||||
).catch(() => null);
|
||||
|
||||
// Click on the header to expand it
|
||||
await header.click();
|
||||
testLogger.info(`Clicked on field header: ${fieldName}`);
|
||||
|
||||
const response = await valuesResponsePromise;
|
||||
if (response) {
|
||||
testLogger.info(`Values API responded for field: ${fieldName}`);
|
||||
await page.waitForTimeout(2000);
|
||||
return { fieldName, success: true };
|
||||
} else {
|
||||
testLogger.info(`No values API response for field: ${fieldName}, trying next field`);
|
||||
// Click again to collapse and try another field
|
||||
await header.click().catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
testLogger.info('Fields table not visible, trying fallback selectors');
|
||||
// Fallback: try finding expansion items anywhere on the page
|
||||
const anyExpansionItems = page.locator('.field-expansion-item');
|
||||
const count = await anyExpansionItems.count();
|
||||
testLogger.info(`Fallback: found ${count} field-expansion-item elements on page`);
|
||||
}
|
||||
|
||||
// Strategy 3: Fallback - try clicking directly on field names
|
||||
testLogger.info('Strategies 1-2 failed, no field could be expanded with values API response');
|
||||
return { fieldName: null, success: false };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TRACES AUTOCOMPLETE TESTS
|
||||
// ============================================================================
|
||||
|
||||
test.describe("Traces Autocomplete Value Suggestions", () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
let pm;
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Navigate to traces page
|
||||
await pm.tracesPage.navigateToTraces();
|
||||
|
||||
// Select the default stream if visible
|
||||
if (await pm.tracesPage.isStreamSelectVisible()) {
|
||||
await pm.tracesPage.selectTraceStream('default');
|
||||
}
|
||||
|
||||
testLogger.info('Traces test setup completed');
|
||||
});
|
||||
|
||||
test("should capture field values to IndexedDB when expanding a field in traces sidebar", {
|
||||
tag: ['@all', '@autosuggestions', '@traces', '@indexeddb']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing traces field expansion capture');
|
||||
|
||||
// Clear IndexedDB
|
||||
await clearIndexedDB(page);
|
||||
|
||||
// Use setupTraceSearch - the standard pattern from working traces tests
|
||||
await pm.tracesPage.setupTraceSearch('default');
|
||||
|
||||
// Verify we have trace results
|
||||
const hasResults = await pm.tracesPage.hasTraceResults();
|
||||
if (!hasResults) {
|
||||
// Check for no results message which is also a valid state
|
||||
const hasNoResults = await pm.tracesPage.isNoResultsVisible();
|
||||
if (hasNoResults) {
|
||||
testLogger.info('No trace data available - skipping field expansion test');
|
||||
test.skip(true, 'No trace data available in test environment');
|
||||
return;
|
||||
}
|
||||
testLogger.info('Search did not complete - test precondition not met');
|
||||
test.skip(true, 'Trace search did not complete - precondition not met');
|
||||
return;
|
||||
}
|
||||
|
||||
testLogger.info('Trace results found, attempting field expansion');
|
||||
|
||||
// Ensure field list is visible
|
||||
const fieldListVisible = await pm.tracesPage.isIndexListVisible();
|
||||
if (!fieldListVisible) {
|
||||
testLogger.info('Field list not visible, toggling...');
|
||||
await pm.tracesPage.toggleFieldList();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// Try to expand a field
|
||||
const expandResult = await tryExpandField(page, pm);
|
||||
|
||||
if (!expandResult.success) {
|
||||
// This is acceptable - some environments may not have expandable fields
|
||||
testLogger.info('No expandable fields found - this may be expected in some environments');
|
||||
test.skip(true, 'No expandable fields found in test environment');
|
||||
return;
|
||||
}
|
||||
|
||||
testLogger.info(`Successfully expanded field: ${expandResult.fieldName}`);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check IndexedDB for traces records
|
||||
const records = await getIndexedDBRecords(page);
|
||||
const traceRecords = records.filter(r => r.key && r.key.includes('|traces|'));
|
||||
|
||||
testLogger.info(`Found ${traceRecords.length} traces records in IndexedDB`);
|
||||
|
||||
// Assert we captured trace records - no silent passing
|
||||
expect(traceRecords.length, 'Expected traces records to be captured in IndexedDB after field expansion').toBeGreaterThan(0);
|
||||
|
||||
if (traceRecords.length > 0) {
|
||||
testLogger.info('Sample traces records:');
|
||||
traceRecords.slice(0, 3).forEach(r => {
|
||||
testLogger.info(` - ${r.key}: ${r.values?.length || 0} values`);
|
||||
});
|
||||
}
|
||||
|
||||
testLogger.info('Traces field expansion capture test completed');
|
||||
});
|
||||
|
||||
test("should show value suggestions in traces query editor", {
|
||||
tag: ['@all', '@autosuggestions', '@traces', '@autocomplete']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing traces autocomplete suggestions');
|
||||
|
||||
// Use setupTraceSearch - the standard pattern
|
||||
await pm.tracesPage.setupTraceSearch('default');
|
||||
|
||||
// Verify we have trace results
|
||||
const hasResults = await pm.tracesPage.hasTraceResults();
|
||||
if (!hasResults) {
|
||||
const hasNoResults = await pm.tracesPage.isNoResultsVisible();
|
||||
if (hasNoResults) {
|
||||
testLogger.info('No trace data available - skipping autocomplete test');
|
||||
test.skip(true, 'No trace data available in test environment');
|
||||
}
|
||||
testLogger.info('Search did not complete - test precondition not met');
|
||||
test.skip(true, 'Search did not complete - test precondition not met');
|
||||
}
|
||||
|
||||
// Try to expand a field to capture values
|
||||
const expandResult = await tryExpandField(page, pm);
|
||||
let fieldToQuery = 'service.name'; // default fallback
|
||||
|
||||
if (expandResult.success && expandResult.fieldName !== 'unknown') {
|
||||
fieldToQuery = expandResult.fieldName;
|
||||
await page.waitForTimeout(2000);
|
||||
testLogger.info(`Expanded field for value capture: ${fieldToQuery}`);
|
||||
}
|
||||
|
||||
// Enter query in the query editor using tracesPage method
|
||||
await pm.tracesPage.enterTraceQuery(`${fieldToQuery} = `);
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Control+Space');
|
||||
|
||||
// Wait for suggestions widget and assert suggestions appear
|
||||
await waitForSuggestionsWidget(page, 10000);
|
||||
const suggestions = await getSuggestionLabels(page);
|
||||
testLogger.info(`Traces suggestions: ${suggestions.slice(0, 5).join(', ')}`);
|
||||
|
||||
// Assert suggestions appeared - no silent passing
|
||||
expect(suggestions.length, 'Expected autocomplete suggestions to appear').toBeGreaterThan(0);
|
||||
testLogger.info('✅ Autocomplete suggestions appeared');
|
||||
|
||||
testLogger.info('Traces autocomplete test completed');
|
||||
});
|
||||
|
||||
test("should isolate traces values from logs values", {
|
||||
tag: ['@all', '@autosuggestions', '@traces', '@isolation']
|
||||
}, async ({ page }) => {
|
||||
testLogger.info('Testing traces vs logs isolation');
|
||||
|
||||
// First, capture some traces values by expanding a field
|
||||
await pm.tracesPage.setupTraceSearch('default');
|
||||
|
||||
// Verify we have trace results
|
||||
const hasResults = await pm.tracesPage.hasTraceResults();
|
||||
if (hasResults) {
|
||||
// Try to expand a field to capture values
|
||||
const expandResult = await tryExpandField(page, pm);
|
||||
if (expandResult.success) {
|
||||
testLogger.info(`Expanded field for isolation test: ${expandResult.fieldName}`);
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current IndexedDB state
|
||||
const records = await getIndexedDBRecords(page);
|
||||
|
||||
// Separate logs and traces records
|
||||
const logsRecords = records.filter(r => r.key && r.key.includes('|logs|'));
|
||||
const tracesRecords = records.filter(r => r.key && r.key.includes('|traces|'));
|
||||
|
||||
testLogger.info(`Logs records: ${logsRecords.length}`);
|
||||
testLogger.info(`Traces records: ${tracesRecords.length}`);
|
||||
|
||||
// Verify keys are properly namespaced
|
||||
let isolationValid = true;
|
||||
|
||||
for (const record of tracesRecords) {
|
||||
const keyParts = record.key.split('|');
|
||||
if (keyParts.length >= 4) {
|
||||
const streamType = keyParts[1];
|
||||
if (streamType !== 'traces') {
|
||||
isolationValid = false;
|
||||
testLogger.warn(`Traces record has wrong namespace: ${record.key}`);
|
||||
} else {
|
||||
testLogger.info(`✅ Traces record properly namespaced: ${record.key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of logsRecords) {
|
||||
const keyParts = record.key.split('|');
|
||||
if (keyParts.length >= 4) {
|
||||
const streamType = keyParts[1];
|
||||
if (streamType !== 'logs') {
|
||||
isolationValid = false;
|
||||
testLogger.warn(`Logs record has wrong namespace: ${record.key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assert isolation is valid - require at least some records to verify
|
||||
if (tracesRecords.length > 0 || logsRecords.length > 0) {
|
||||
expect(isolationValid, 'Stream type isolation should be maintained').toBe(true);
|
||||
testLogger.info('✅ Stream type isolation verified with records');
|
||||
} else {
|
||||
// No records means we can't verify isolation - skip the test
|
||||
testLogger.warn('No records to verify isolation - skipping test');
|
||||
test.skip(true, 'Isolation test requires records - run field expansion tests first');
|
||||
}
|
||||
|
||||
testLogger.info('Isolation test completed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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);
|
||||
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);
|
||||
await this.page.keyboard.press('Control+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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 pressing Escape
|
||||
*/
|
||||
async dismissSuggestions() {
|
||||
await this.page.keyboard.press('Escape');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonacoEditorHelper;
|
||||
Reference in New Issue
Block a user