From d6b5630ed68cef19ef6b319e78de250721244d39 Mon Sep 17 00:00:00 2001 From: Anan Date: Mon, 3 Feb 2025 06:15:07 -0800 Subject: [PATCH 1/2] [TESTID-64,80,UI] Add cypress test for auto query updates when switch dataset Add tests related to autocomplete. This will close all the issues listed here: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/assigned/ananzh?q=is%3Aissue%20assignee%3Aananzh%20label%3A%22discover%20autocomplete%22%20 Signed-off-by: Anan --- .../autocomplete_query.spec.js | 98 ++++ .../autocomplete_switch.spec.js | 123 ++++ .../autocomplete_ui.spec.js | 145 +++++ .../apps/query_enhancements/autocomplete.js | 553 ++++++++++++++++++ .../utils/apps/query_enhancements/commands.js | 86 ++- docs/_sidebar.md | 1 + package.json | 4 +- 7 files changed, 999 insertions(+), 11 deletions(-) create mode 100644 cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_query.spec.js create mode 100644 cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_switch.spec.js create mode 100644 cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_ui.spec.js create mode 100644 cypress/utils/apps/query_enhancements/autocomplete.js diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_query.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_query.spec.js new file mode 100644 index 000000000000..4dc12739b53a --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_query.spec.js @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + QueryLanguages, + PATHS, + DATASOURCE_NAME, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + setDatePickerDatesAndSearchIfRelevant, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { + validateQueryResults, + generateAutocompleteTestConfiguration, + generateAutocompleteTestConfigurations, + createOtherQueryUsingAutocomplete, + createDQLQueryUsingAutocomplete, +} from '../../../../../utils/apps/query_enhancements/autocomplete'; +import { prepareTestSuite } from '../../../../../utils/helpers'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runAutocompleteTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteAllWorkspaces(); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + }); + + generateAutocompleteTestConfigurations(generateAutocompleteTestConfiguration).forEach( + (config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === 'INDEX_PATTERN') { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('should show and select suggestions progressively', function () { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + + if (config.language === QueryLanguages.DQL.name) { + createDQLQueryUsingAutocomplete(); + } else { + createOtherQueryUsingAutocomplete(config); + } + + // Run the query + cy.getElementByTestId('querySubmitButton').click(); + cy.waitForLoader(true); + cy.wait(1000); + // Validate results meet our conditions + validateQueryResults('bytes_transferred', 9500, '>'); + validateQueryResults('category', 'Application'); + }); + }); + } + ); + }); +}; + +prepareTestSuite('Autocomplete Query', runAutocompleteTests); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_switch.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_switch.spec.js new file mode 100644 index 000000000000..8e612445cf5d --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_switch.spec.js @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + INDEX_WITH_TIME_2, + QueryLanguages, + PATHS, + DATASOURCE_NAME, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + getDefaultQuery, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { + generateAutocompleteTestConfiguration, + generateAutocompleteTestConfigurations, + LanguageConfigs, + getDatasetName, +} from '../../../../../utils/apps/query_enhancements/autocomplete'; +import { prepareTestSuite } from '../../../../../utils/helpers'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runAutocompleteTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_2}.mapping.json`, + ], + [ + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`, + `cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_2}.data.ndjson`, + ] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteAllWorkspaces(); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + cy.osd.deleteIndex(INDEX_WITH_TIME_2); + }); + + generateAutocompleteTestConfigurations(generateAutocompleteTestConfiguration, { + languageConfig: LanguageConfigs.SQL_PPL, + }).forEach((config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === 'INDEX_PATTERN') { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_2, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('should update default query when switching index patterns and languages', function () { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + + // Get dataset names based on type + const firstDataset = getDatasetName('data_logs_small_time_1', config.datasetType); + const secondDataset = getDatasetName('data_logs_small_time_2', config.datasetType); + + // Verify initial default query + cy.getElementByTestId('osdQueryEditor__multiLine').contains( + getDefaultQuery(firstDataset, config.language) + ); + + // Switch to second index pattern + cy.setDataset(secondDataset, DATASOURCE_NAME, config.datasetType); + + // Verify query updated for new index pattern + cy.getElementByTestId('osdQueryEditor__multiLine').contains( + getDefaultQuery(secondDataset, config.language) + ); + + // Switch language and verify index pattern maintained + const switchLanguage = + config.language === QueryLanguages.SQL.name + ? QueryLanguages.PPL.name + : QueryLanguages.SQL.name; + cy.setQueryLanguage(switchLanguage); + cy.getElementByTestId('osdQueryEditor__multiLine').contains( + getDefaultQuery(secondDataset, switchLanguage) + ); + }); + }); + }); + }); +}; + +prepareTestSuite('Autocomplete Switch', runAutocompleteTests); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_ui.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_ui.spec.js new file mode 100644 index 000000000000..8295d8f26a86 --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/autocomplete_ui.spec.js @@ -0,0 +1,145 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_WITH_TIME_1, + QueryLanguages, + PATHS, + DATASOURCE_NAME, +} from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + setDatePickerDatesAndSearchIfRelevant, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { + generateAutocompleteTestConfiguration, + generateAutocompleteTestConfigurations, + validateQueryResults, + showSuggestionAndHint, + hideWidgets, + createQuery, +} from '../../../../../utils/apps/query_enhancements/autocomplete'; +import { prepareTestSuite } from '../../../../../utils/helpers'; + +const workspaceName = getRandomizedWorkspaceName(); + +export const runAutocompleteTests = () => { + describe('discover autocomplete tests', () => { + beforeEach(() => { + cy.osd.setupTestData( + PATHS.SECONDARY_ENGINE, + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] + ); + cy.osd.addDataSource({ + name: DATASOURCE_NAME, + url: PATHS.SECONDARY_ENGINE, + authType: 'no_auth', + }); + cy.deleteAllWorkspaces(); + cy.visit('/app/home'); + cy.osd.createInitialWorkspaceWithDataSource(DATASOURCE_NAME, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + cy.osd.deleteDataSourceByName(DATASOURCE_NAME); + cy.osd.deleteIndex(INDEX_WITH_TIME_1); + cy.window().then((win) => { + win.localStorage.clear(); + win.sessionStorage.clear(); + }); + }); + + generateAutocompleteTestConfigurations(generateAutocompleteTestConfiguration).forEach( + (config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === 'INDEX_PATTERN') { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_WITH_TIME_1, + timefieldName: 'timestamp', + dataSource: DATASOURCE_NAME, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + it('should verify suggestion widget and its hint', function () { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + const editorType = + config.language === QueryLanguages.DQL.name + ? 'osdQueryEditor__singleLine' + : 'osdQueryEditor__multiLine'; + + cy.getElementByTestId(editorType) + .find('.monaco-editor') + .should('be.visible') + .within(() => { + // Show suggestion and hint with retry + showSuggestionAndHint(); + + // Verify suggestions are visible + cy.get('.monaco-list-row').should('be.visible').should('have.length.at.least', 1); + + // Sends ESC and verifies widgets are hidden + hideWidgets(); + + // TODO: Add test for having another focused window after bug is fixed + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8973 + }); + }); + + it('should build query using mouse interactions', function () { + // Setup + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + + createQuery(config, false); // use mouse + + // Run with mouse click + cy.getElementByTestId('querySubmitButton').click(); + + cy.waitForLoader(true); + cy.wait(1000); + validateQueryResults('unique_category', 'Configuration'); + }); + + it('should build query using keyboard shortcuts', function () { + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + cy.clearQueryEditor(); + + createQuery(config, true); // use keyboard + + // Run with keyboard shortcut + cy.get('.inputarea').type( + config.language === QueryLanguages.DQL.name ? '{enter}' : '{cmd+enter}' + ); + + cy.waitForLoader(true); + cy.wait(1000); + validateQueryResults('unique_category', 'Configuration'); + }); + }); + } + ); + }); +}; + +prepareTestSuite('Autocomplete UI', runAutocompleteTests); diff --git a/cypress/utils/apps/query_enhancements/autocomplete.js b/cypress/utils/apps/query_enhancements/autocomplete.js new file mode 100644 index 000000000000..05c53b916e6f --- /dev/null +++ b/cypress/utils/apps/query_enhancements/autocomplete.js @@ -0,0 +1,553 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + QueryLanguages, + INDEX_WITH_TIME_1, + INDEX_PATTERN_WITH_TIME_1, + DatasetTypes, +} from './constants'; + +// ======================================= +// Common Utilities (Used across multiple autocomplete specs) +// ======================================= + +/** + * Gets the dataset name based on the dataset type + * @param {string} baseName - Base name of the dataset + * @param {string} datasetType - Type of dataset (INDEX_PATTERN or INDEXES) + * @returns {string} Formatted dataset name + */ +export const getDatasetName = (baseName, datasetType) => { + return datasetType === DatasetTypes.INDEX_PATTERN.name ? `${baseName}*` : baseName; +}; + +/** + * Types input and verifies expected suggestion appears in suggestion list + * @param {string} input - Text to type + * @param {string} expectedSuggestion - Expected suggestion to verify + */ +export const typeAndVerifySuggestion = (input, expectedSuggestion) => { + if (input) { + cy.get('.inputarea').type(input, { force: true }); + } + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row').should('exist').contains(expectedSuggestion); + }); +}; + +/** + * Finds and selects a specific suggestion from the suggestion list + * @param {string} suggestionText - Text of suggestion to select + */ +export const selectSpecificSuggestion = (suggestionText) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row').contains(suggestionText).should('be.visible').click(); + }); +}; + +/** + * Types input, verifies suggestion exists, and selects it + * @param {string} input - Text to type + * @param {string} expectedSuggestion - Expected suggestion to verify and select + */ +export const typeAndSelectSuggestion = (input, expectedSuggestion) => { + typeAndVerifySuggestion(input, expectedSuggestion); + selectSpecificSuggestion(expectedSuggestion); +}; + +/** + * Shows suggestions by clicking in the editor + */ +export const showSuggestions = () => { + cy.get('.inputarea').click(); + cy.get('.suggest-widget').should('be.visible'); +}; + +/** + * Verifies all expected values appear in suggestion list + * @param {string[]} expectedValues - Array of values to verify + */ +export const verifyFieldValues = (expectedValues) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + expectedValues.forEach((value) => { + cy.get('.monaco-list-row').should('contain', value); + }); + }); +}; + +// ======================================= +// Autocomplete Query Spec Utilities +// ======================================= + +/** + * Handles category field suggestions flow for query building + * Used in createOtherQueryUsingAutocomplete and createDQLQueryUsingAutocomplete + */ +const handleCategoryFieldSuggestions = () => { + const expectedCategoryValues = ['Application', 'Database', 'Network', 'Security']; + typeAndSelectSuggestion('c', 'category'); + typeAndVerifySuggestion('', '='); + selectSpecificSuggestion('='); + verifyFieldValues(expectedCategoryValues); + typeAndSelectSuggestion('App', 'Application'); +}; + +/** + * Gets language-specific configuration for query building + * @param {string} language - Query language + * @param {Object} config - Configuration object + * @returns {Object} Language-specific configuration + */ +const getLanguageSpecificConfig = (language, config) => { + const datasetName = getDatasetName('data_logs_small_time_1', config.datasetType); + cy.log(`Dataset name for ${language} with type ${config.datasetType}: ${datasetName}`); + + switch (language) { + case QueryLanguages.PPL.name: + return { + initialCommands: [ + { value: 'source', input: 's' }, + { value: '=' }, + { value: getDatasetName('data_logs_small_time_1', config.datasetType) }, + { value: '|' }, + { value: 'where', input: 'w' }, + ], + editorType: 'osdQueryEditor__multiLine', + andOperator: 'and', + }; + case QueryLanguages.SQL.name: + return { + initialCommands: [ + { value: 'SELECT', input: 's' }, + { value: '*' }, + { value: 'FROM', input: 'f' }, + { value: getDatasetName('data_logs_small_time_1', config.datasetType) }, + { value: 'WHERE', input: 'w' }, + ], + editorType: 'osdQueryEditor__multiLine', + andOperator: 'AND', + }; + default: + throw new Error(`Unsupported language: ${language}`); + } +}; + +/** + * Creates a query using autocomplete for SQL/PPL languages + * @param {Object} config - Query configuration object + */ +export const createOtherQueryUsingAutocomplete = (config) => { + const langConfig = getLanguageSpecificConfig(config.language, config); + + cy.getElementByTestId(langConfig.editorType) + .find('.monaco-editor') + .should('be.visible') + .should('have.class', 'vs') + .wait(1000) + .within(() => { + // Handle initial language-specific setup + langConfig.initialCommands.forEach((command) => { + if (command.input) { + typeAndSelectSuggestion(command.input, command.value); + } else { + typeAndVerifySuggestion('', command.value); + selectSpecificSuggestion(command.value); + } + }); + + handleCategoryFieldSuggestions(); + + // Handle common ending pattern + typeAndSelectSuggestion('a', langConfig.andOperator); + typeAndVerifySuggestion('', 'bytes_transferred'); + selectSpecificSuggestion('bytes_transferred'); + + if (config.language === QueryLanguages.SQL.name) { + // Handle operator (different for SQL vs PPL) + // TODO: SQL doesn't support operator suggestions yet except for '=' + cy.get('.inputarea').type('> ', { force: true }); + } else { + typeAndVerifySuggestion('', '>'); + selectSpecificSuggestion('>'); + } + + cy.get('.inputarea').type('9500', { force: true }); + }); +}; + +/** + * Creates a DQL query using autocomplete + */ +export const createDQLQueryUsingAutocomplete = () => { + cy.getElementByTestId('osdQueryEditor__singleLine') + .find('.monaco-editor') + .should('be.visible') + .should('have.class', 'vs') + .wait(1000) + .within(() => { + typeAndSelectSuggestion('c', 'category'); + const expectedCategoryValues = ['Application', 'Database', 'Network', 'Security']; + verifyFieldValues(expectedCategoryValues); + typeAndSelectSuggestion('App', 'Application'); + typeAndSelectSuggestion('a', 'and'); + typeAndVerifySuggestion('', 'bytes_transferred'); + selectSpecificSuggestion('bytes_transferred'); + typeAndVerifySuggestion('', '>'); + selectSpecificSuggestion('>'); + cy.get('.inputarea').type('9500', { force: true }); + }); +}; + +/** + * Clicks to select a specific suggestion from the suggestion list + * Used by: autocomplete_query.spec.js + * @param {string} suggestionText - Text of suggestion to select + */ +export const clickSuggestion = (suggestionText) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row').contains(suggestionText).should('be.visible').click(); + }); +}; + +/** + * Shows suggestions and clicks to select one + * Used by: autocomplete_query.spec.js + * @param {string} expectedSuggestion - Suggestion to select + */ +export const showAndClickSuggestion = (expectedSuggestion) => { + showSuggestions(); + clickSuggestion(expectedSuggestion); +}; + +/** + * Shows suggestion and scrolls if needed before clicking + * Used by: autocomplete_query.spec.js + * @param {string} suggestionText - Text of suggestion to select + */ +export const showAndClickSuggestionWithScroll = (suggestionText) => { + cy.get('.suggest-widget') + .should('be.visible') + .within(() => { + cy.get('.monaco-list-row') + .contains(suggestionText) + .scrollIntoView() + .should('be.visible') + .click(); + }); +}; + +// ======================================= +// Autocomplete UI Spec Utilities +// ======================================= + +/** + * Selects a suggestion using keyboard or mouse with retry logic + * @param {string} suggestionText - Text of suggestion to select + * @param {boolean} useKeyboard - Whether to use keyboard instead of mouse + */ +export const selectSuggestion = (suggestionText, useKeyboard = false) => { + cy.log(`Selecting suggestion "${suggestionText}"`); + const maxAttempts = 30; + + const findAndSelectSuggestion = (attempt = 0) => { + if (attempt >= maxAttempts) { + throw new Error( + `Could not find suggestion "${suggestionText}" after ${maxAttempts} attempts` + ); + } + + return cy.get('.monaco-list-row').then(($rows) => { + const isVisible = $rows + .toArray() + .some((row) => Cypress.$(row).text().includes(suggestionText)); + + if (isVisible) { + if (useKeyboard) { + const highlightedRow = $rows.filter('.focused').text(); + if (highlightedRow.includes(suggestionText)) { + return cy.get('.inputarea').trigger('keydown', { + key: 'Tab', + keyCode: 9, + which: 9, + force: true, + }); + } + } else { + return cy.get('.monaco-list-row').contains(suggestionText).click({ force: true }); + } + } + + return cy + .get('.inputarea') + .type('{downarrow}', { force: true }) + .wait(50) + .then(() => findAndSelectSuggestion(attempt + 1)); + }); + }; + + cy.get('.suggest-widget') + .should('exist') + .should('be.visible') + .should('have.class', 'visible') + .then(() => { + findAndSelectSuggestion(0); + }); +}; + +/** + * Shows suggestion widget and waits for hint to appear with retry logic + * @param {number} maxAttempts - Maximum number of retry attempts + * @returns {Cypress.Chainable} + */ +export const showSuggestionAndHint = (maxAttempts = 3) => { + let attempts = 0; + + const attemptShow = () => { + attempts++; + cy.get('.inputarea').type(' ', { force: true }); + + return cy.get('.suggest-widget.visible').then(($widget) => { + const isVisible = $widget.is(':visible'); + const styles = window.getComputedStyle($widget[0], '::after'); + const hasHint = styles.getPropertyValue('content').includes('Tab to insert'); + + if (!isVisible || !hasHint) { + if (attempts >= maxAttempts) { + throw new Error('Failed to show suggestion and hint after ${maxAttempts} attempts'); + } + return cy.wait(200).then(attemptShow); + } + }); + }; + + return attemptShow(); +}; + +/** + * Hides suggestion widgets with retry logic + * @param {number} maxAttempts - Maximum number of retry attempts + * @returns {Cypress.Chainable} + */ +export const hideWidgets = (maxAttempts = 3) => { + let attempts = 0; + + const attemptHide = () => { + attempts++; + cy.get('.inputarea').type('{esc}', { force: true }); + + return cy.get('.suggest-widget').then(($widget) => { + if ($widget.hasClass('visible')) { + if (attempts >= maxAttempts) { + throw new Error('Failed to hide widgets after ${maxAttempts} attempts'); + } + return cy.wait(200).then(attemptHide); + } + }); + }; + + return attemptHide(); +}; + +/** + * Creates a query using either mouse or keyboard interactions + * @param {Object} config - Query configuration + * @param {boolean} useKeyboard - Whether to use keyboard instead of mouse + */ +export const createQuery = (config, useKeyboard = false) => { + const editorType = + config.language === QueryLanguages.DQL.name + ? 'osdQueryEditor__singleLine' + : 'osdQueryEditor__multiLine'; + + cy.getElementByTestId(editorType) + .find('.monaco-editor') + .should('be.visible') + .should('have.class', 'vs') + .wait(1000) + .within(() => { + cy.get('.inputarea').type(' ', { force: true }); + if (config.language === QueryLanguages.PPL.name) { + selectSuggestion('source', useKeyboard); + selectSuggestion('=', useKeyboard); + const dataset = getDatasetName('data_logs_small_time_1', config.datasetType); + selectSuggestion(dataset, useKeyboard); + selectSuggestion('|', useKeyboard); + selectSuggestion('where', useKeyboard); + selectSuggestion('unique_category', useKeyboard); + selectSuggestion('=', useKeyboard); + selectSuggestion('Configuration', useKeyboard); + } else if (config.language === QueryLanguages.SQL.name) { + selectSuggestion('SELECT', useKeyboard); + selectSuggestion('*', useKeyboard); + selectSuggestion('FROM', useKeyboard); + selectSuggestion('data_logs_small_time_1', useKeyboard); + selectSuggestion('WHERE', useKeyboard); + selectSuggestion('unique_category', useKeyboard); + selectSuggestion('=', useKeyboard); + selectSuggestion('Configuration', useKeyboard); + } else if (config.language === QueryLanguages.DQL.name) { + selectSuggestion('unique_category', useKeyboard); + selectSuggestion('Configuration', useKeyboard); + } + }); +}; + +// ======================================= +// Test Configuration Generators +// ======================================= + +/** + * Language configurations for different test scenarios + */ +export const LanguageConfigs = { + SQL_PPL: { + INDEX_PATTERN: [QueryLanguages.SQL, QueryLanguages.PPL], + INDEXES: [QueryLanguages.SQL, QueryLanguages.PPL], + }, + SQL_PPL_DQL: { + INDEX_PATTERN: [QueryLanguages.DQL, QueryLanguages.SQL, QueryLanguages.PPL], + INDEXES: [QueryLanguages.SQL, QueryLanguages.PPL], + }, +}; + +/** + * Creates dataset types configuration for autocomplete tests + * @param {Object} languageConfig - Language configuration object + * @returns {Object} Dataset types configuration + */ +const createAutocompleteDatasetTypes = (languageConfig = LanguageConfigs.SQL_PPL) => ({ + INDEX_PATTERN: { + name: 'INDEX_PATTERN', + supportedLanguages: languageConfig.INDEX_PATTERN, + }, + INDEXES: { + name: 'INDEXES', + supportedLanguages: languageConfig.INDEXES, + }, +}); + +export const AutocompleteDatasetTypes = createAutocompleteDatasetTypes(); + +// ======================================= +// Test Configuration Generators and other common utilities +// ======================================= + +/** + * Generates base test configuration for autocomplete tests + * Used by: autocomplete_query.spec.js, autocomplete_switch.spec.js, autocomplete_ui.spec.js + * @param {string} dataset - Dataset name + * @param {string} datasetType - Type of dataset + * @param {Object} language - Language configuration + * @returns {Object} Test configuration object + */ +export const generateAutocompleteTestConfiguration = (dataset, datasetType, language) => { + const baseConfig = { + dataset, + datasetType, + language: language.name, + testName: `${language.name}-${datasetType}`, + }; + + return { + ...baseConfig, + }; +}; + +/** + * Generates test configurations for autocomplete tests across different dataset types + * Used by: autocomplete_query.spec.js, autocomplete_switch.spec.js, autocomplete_ui.spec.js + * @param {Function} generateTestConfigurationCallback - Callback function to generate test config + * @param {Object} options - Configuration options + * @param {string} [options.indexPattern] - Custom index pattern name + * @param {string} [options.index] - Custom index name + * @param {Object} [options.languageConfig] - Custom language configuration + * @returns {Array} Array of test configurations + */ +export const generateAutocompleteTestConfigurations = ( + generateTestConfigurationCallback, + options = {} +) => { + const { + indexPattern = INDEX_PATTERN_WITH_TIME_1, + index = INDEX_WITH_TIME_1, + languageConfig = LanguageConfigs.SQL_PPL_DQL, + } = options; + + const datasetTypes = createAutocompleteDatasetTypes(languageConfig); + + return Object.values(datasetTypes).flatMap((dataset) => + dataset.supportedLanguages.map((language) => { + let datasetToUse; + switch (dataset.name) { + case datasetTypes.INDEX_PATTERN.name: + datasetToUse = indexPattern; + break; + case datasetTypes.INDEXES.name: + datasetToUse = index; + break; + default: + throw new Error( + `generateAutocompleteTestConfigurations encountered unsupported dataset: ${dataset.name}` + ); + } + return generateTestConfigurationCallback(datasetToUse, dataset.name, language); + }) + ); +}; + +/** + * Validates query results by comparing field values with expected values using specified operator + * Used by: autocomplete_query.spec.js, autocomplete_ui.spec.js + * @param {string} field - The field name to validate + * @param {number|string} expectedValue - The value to compare against + * @param {string} [operator] - The operator to use for comparison ('>', '<', '=', or undefined for equality) + */ +export const validateQueryResults = (field, expectedValue, operator) => { + // Expand the first row to view the field value + cy.get('tbody tr').first().find('[data-test-subj="docTableExpandToggleColumn"] button').click(); + cy.getElementByTestId(`tableDocViewRow-${field}-value`).within(() => { + cy.get('span') + .invoke('text') + .then((text) => { + // For numeric comparisons (>, <, >=, <=) + if (['>', '<', '>=', '<=', '='].includes(operator)) { + const actualValue = parseFloat(text.replace(/,/g, '')); + const numericExpectedValue = parseFloat(expectedValue.toString().replace(/,/g, '')); + + switch (operator) { + case '>': + expect(actualValue).to.be.greaterThan(numericExpectedValue); + break; + case '<': + expect(actualValue).to.be.lessThan(numericExpectedValue); + break; + case '>=': + expect(actualValue).to.be.at.least(numericExpectedValue); + break; + case '<=': + expect(actualValue).to.be.at.most(numericExpectedValue); + break; + case '=': + expect(actualValue).to.equal(numericExpectedValue); + break; + } + } else { + // For undefined, keep original string comparison + expect(text).to.equal(expectedValue.toString()); + } + }); + }); + // Close the expanded row + cy.get('tbody tr').first().find('[data-test-subj="docTableExpandToggleColumn"] button').click(); +}; diff --git a/cypress/utils/apps/query_enhancements/commands.js b/cypress/utils/apps/query_enhancements/commands.js index 5ce71f043f7e..b2d2516f387f 100644 --- a/cypress/utils/apps/query_enhancements/commands.js +++ b/cypress/utils/apps/query_enhancements/commands.js @@ -3,7 +3,80 @@ * SPDX-License-Identifier: Apache-2.0 */ -Cypress.Commands.add('setQueryEditor', (value, opts = {}, submit = true) => { +const MAX_RETRIES = 3; +const RETRY_DELAY = 1000; + +const forceFocusEditor = () => { + return cy + .get('.globalQueryEditor .react-monaco-editor-container') + .click({ force: true }) + .wait(200) // Give editor time to register focus + .get('.inputarea') + .focus() + .wait(200); // Wait for focus to take effect +}; + +const clearMonacoEditor = () => { + return cy + .get('.globalQueryEditor .react-monaco-editor-container') + .should('exist') + .should('be.visible') + .then(() => { + // First ensure we have focus + return forceFocusEditor().then(() => { + // Try different key combinations for selection + return cy + .get('.inputarea') + .type('{ctrl}a', { force: true }) + .wait(100) + .type('{backspace}', { force: true }) + .wait(100) + .type('{meta}a', { force: true }) + .wait(100) + .type('{backspace}', { force: true }); + }); + }); +}; + +const isEditorEmpty = () => { + return cy + .get('.globalQueryEditor .react-monaco-editor-container') + .find('.view-line') + .invoke('text') + .then((text) => text.trim() === ''); +}; + +Cypress.Commands.add('clearQueryEditor', () => { + const clearWithRetry = (attempt = 1) => { + cy.log(`Attempt ${attempt} to clear editor`); + + return forceFocusEditor() + .then(() => clearMonacoEditor()) + .then(() => { + return isEditorEmpty().then((isEmpty) => { + cy.log(`is editor empty: ${isEmpty}`); + + if (isEmpty) { + return; // Editor is cleared, we're done + } + + if (attempt < MAX_RETRIES) { + cy.log(`Editor not cleared, retrying... (attempt ${attempt})`); + cy.wait(RETRY_DELAY); // Wait before next attempt + return clearWithRetry(attempt + 1); + } else { + cy.log('Failed to clear editor after all attempts'); + // Instead of throwing error, try one last time with extra waiting + return cy.wait(2000).then(forceFocusEditor).then(clearMonacoEditor); + } + }); + }); + }; + + return clearWithRetry(); +}); + +Cypress.Commands.add('setQueryEditor', (value, submit = true) => { Cypress.log({ name: 'setQueryEditor', displayName: 'set query', @@ -15,14 +88,9 @@ Cypress.Commands.add('setQueryEditor', (value, opts = {}, submit = true) => { cy.getElementByTestId('headerGlobalNav').click(); // clear the editor first and then set - cy.get('.globalQueryEditor .react-monaco-editor-container') - .click() - .focused() - .type('{ctrl}a') - .type('{backspace}') - .type('{meta}a') - .type('{backspace}') - .type(value, opts); + clearMonacoEditor().then(() => { + return cy.get('.inputarea').should('be.visible').wait(200).type(value, { force: true }); + }); if (submit) { cy.updateTopNav({ log: false }); diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 27442c2e608b..8677d6104bc4 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -196,4 +196,5 @@ - [RELEASING](../RELEASING.md) - [SECURITY](../SECURITY.md) - [TESTING](../TESTING.md) + - [TRIAGING](../TRIAGING.md) - [TYPESCRIPT](../TYPESCRIPT.md) diff --git a/package.json b/package.json index 1bf05c82f1c0..f0f6edbad8c6 100644 --- a/package.json +++ b/package.json @@ -88,8 +88,8 @@ "cypress:run-with-security": "env TZ=America/Los_Angeles NO_COLOR=1 cypress run --env SECURITY_ENABLED=true,openSearchUrl=https://localhost:9200,WAIT_FOR_LOADER_BUFFER_MS=500", "osd:ciGroup10": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/saved_search.spec.js,$BASE_PATH/queries.spec.js,$BASE_PATH/a_check.spec.js,$BASE_PATH/dataset_selector.spec.js,$BASE_PATH/s3_dataset.spec.js,$BASE_PATH/simple_dataset_selector.spec.js\"", "osd:ciGroup11": "echo \"cypress/integration/dashboard_sanity_test.spec.ts\"", - "osd:ciGroup12": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/time_range_selection.spec.js,$BASE_PATH/saved_queries.spec.js,$BASE_PATH/language_specific_display.spec.js,$BASE_PATH/saved_search_in_dashboards.spec.js\"", - "osd:ciGroup13": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/sidebar.spec.js,$BASE_PATH/shared_links.spec.js,$BASE_PATH/table.spec.js,$BASE_PATH/field_display_filtering.spec.js,$BASE_PATH/inspect.spec.js\"", + "osd:ciGroup12": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/time_range_selection.spec.js,$BASE_PATH/saved_queries.spec.js,$BASE_PATH/language_specific_display.spec.js,$BASE_PATH/saved_search_in_dashboards.spec.js,autocomplete_query.spec.js\"", + "osd:ciGroup13": "BASE_PATH='cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements' && echo \"$BASE_PATH/sidebar.spec.js,$BASE_PATH/shared_links.spec.js,$BASE_PATH/table.spec.js,$BASE_PATH/field_display_filtering.spec.js,$BASE_PATH/inspect.spec.js,$BASE_PATH/autocomplete_switch.spec.js,$BASE_PATH/autocomplete_ui.spec.js\"", "generate:opensearchsqlantlr": "./node_modules/antlr4ng-cli/index.js -Dlanguage=TypeScript -o ./src/plugins/data/public/antlr/opensearch_sql/.generated -visitor -no-listener -Xexact-output-dir ./src/plugins/data/public/antlr/opensearch_sql/grammar/OpenSearchSQLLexer.g4 ./src/plugins/data/public/antlr/opensearch_sql/grammar/OpenSearchSQLParser.g4", "generate:opensearchpplantlr": "./node_modules/antlr4ng-cli/index.js -Dlanguage=TypeScript -o ./src/plugins/data/public/antlr/opensearch_ppl/.generated -visitor -no-listener -Xexact-output-dir ./src/plugins/data/public/antlr/opensearch_ppl/grammar/OpenSearchPPLLexer.g4 ./src/plugins/data/public/antlr/opensearch_ppl/grammar/OpenSearchPPLParser.g4" }, From fb465cec44b04fa14f1f54ffd2b76b0e6550ba10 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 23:29:33 +0000 Subject: [PATCH 2/2] Changeset file for PR #9322 created/updated --- changelogs/fragments/9322.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/9322.yml diff --git a/changelogs/fragments/9322.yml b/changelogs/fragments/9322.yml new file mode 100644 index 000000000000..e73b488f1a1f --- /dev/null +++ b/changelogs/fragments/9322.yml @@ -0,0 +1,2 @@ +test: +- [TESTID-64] Add cypress test for auto query updates when switch dataset ([#9322](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9322)) \ No newline at end of file