From 871b4746bc037641c263d6bdc79537f73586a088 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Fri, 20 Dec 2024 16:21:17 -0800 Subject: [PATCH 1/5] Add tests for saved search creation and loading for query enhancement Signed-off-by: Justin Kim --- .../apps/query_enhancements/constants.js | 4 + .../dataset_selector.spec.js | 5 +- .../apps/query_enhancements/queries.spec.js | 9 +- .../query_enhancements/saved_search.spec.js | 513 ++++++++++++++++++ .../utils/apps/query_enhancements/commands.js | 14 +- .../utils/apps/query_enhancements/index.d.ts | 2 +- cypress/utils/dashboards/commands.js | 13 +- cypress/utils/dashboards/index.d.ts | 5 +- 8 files changed, 541 insertions(+), 24 deletions(-) create mode 100644 cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js index b59f85ec4f4..07612aba16e 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js @@ -7,3 +7,7 @@ export const DATASOURCE_NAME = 'query-cluster'; export const WORKSPACE_NAME = 'query-workspace'; export const START_TIME = 'Jan 1, 2020 @ 00:00:00.000'; export const END_TIME = 'Jan 1, 2024 @ 00:00:00.000'; + +export const INDEX_WITH_TIME_1 = 'data_logs_small_time_1'; +export const INDEX_WITH_TIME_2 = 'data_logs_small_time_2'; +export const INDEX_PATTERN_WITH_TIME = 'data_logs_small_time_*'; diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js index cf0144e08b6..f5fd480e7a4 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js @@ -111,8 +111,7 @@ describe('dataset selector', { scrollBehavior: false }, () => { it('create index pattern and select it', function () { // Create and select index pattern for data_logs_small_time_1* cy.createWorkspaceIndexPatterns({ - url: `${BASE_PATH}`, - workspaceName: `${WORKSPACE_NAME}`, + workspaceName: WORKSPACE_NAME, indexPattern: 'data_logs_small_time_1', timefieldName: 'timestamp', indexPatternHasTimefield: true, @@ -120,7 +119,7 @@ describe('dataset selector', { scrollBehavior: false }, () => { isEnhancement: true, }); - cy.navigateToWorkSpaceHomePage(`${BASE_PATH}`, `${WORKSPACE_NAME}`); + cy.navigateToWorkSpaceHomePage(WORKSPACE_NAME); cy.waitForLoader(true); cy.getElementByTestId(`datasetSelectorButton`).click(); diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js index 343ac113120..882ebb3ba3c 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js @@ -29,8 +29,7 @@ describe('query enhancement queries', { scrollBehavior: false }, () => { // Create and select index pattern for data_logs_small_time_1* cy.createWorkspaceIndexPatterns({ - url: `${BASE_PATH}`, - workspaceName: `${WORKSPACE_NAME}`, + workspaceName: WORKSPACE_NAME, indexPattern: 'data_logs_small_time_1', timefieldName: 'timestamp', indexPatternHasTimefield: true, @@ -39,7 +38,7 @@ describe('query enhancement queries', { scrollBehavior: false }, () => { }); // Go to workspace home - cy.navigateToWorkSpaceHomePage(`${BASE_PATH}`, `${WORKSPACE_NAME}`); + cy.navigateToWorkSpaceHomePage(WORKSPACE_NAME); cy.setTopNavDate(START_TIME, END_TIME); cy.waitForLoader(true); }); @@ -55,7 +54,7 @@ describe('query enhancement queries', { scrollBehavior: false }, () => { cy.setQueryLanguage('DQL'); const query = `_id:1`; - cy.setSingleLineQueryEditor(query); + cy.setQueryEditor(query); cy.waitForLoader(true); cy.waitForSearch(); cy.verifyHitCount(1); @@ -69,7 +68,7 @@ describe('query enhancement queries', { scrollBehavior: false }, () => { cy.setQueryLanguage('Lucene'); const query = `_id:1`; - cy.setSingleLineQueryEditor(query); + cy.setQueryEditor(query); cy.waitForLoader(true); cy.waitForSearch(); cy.verifyHitCount(1); diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js new file mode 100644 index 00000000000..5fcdb3040f8 --- /dev/null +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js @@ -0,0 +1,513 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + END_TIME, + START_TIME, + INDEX_PATTERN_WITH_TIME, + INDEX_WITH_TIME_1, + INDEX_WITH_TIME_2, +} from './constants'; +import { SECONDARY_ENGINE } from '../../../../../utils/constants'; +import { v4 as uuid } from 'uuid'; + +const workspaceName = uuid(); +// datasource name must be 32 char or less +const datasourceName = uuid().substring(0, 32); + +const allowedSearchOperations = { + DQL: { + filters: true, + histogram: true, + selectFields: true, + sort: true, + }, + Lucene: { + filters: true, + histogram: true, + selectFields: true, + sort: true, + }, + 'OpenSearch SQL': { + filters: false, + histogram: false, + selectFields: true, + sort: false, + }, + PPL: { + filters: false, + histogram: true, + selectFields: true, + sort: false, + }, +}; + +const indexPatternTestConfigurations = { + DQL: { + ...allowedSearchOperations.DQL, + language: 'DQL', + dataset: INDEX_PATTERN_WITH_TIME, + queryString: 'bytes_transferred > 9950', + hitCount: 28, + sampleTableData: [ + [1, '9,998'], + [2, 'Phyllis Dach'], + ], + saveName: 'dql-index-pattern-01', + }, + Lucene: { + ...allowedSearchOperations.Lucene, + language: 'Lucene', + dataset: INDEX_PATTERN_WITH_TIME, + queryString: 'bytes_transferred: {9950 TO *}', + hitCount: 28, + sampleTableData: [ + [1, '9,998'], + [2, 'Phyllis Dach'], + ], + saveName: 'lucene-index-pattern-01', + }, + 'OpenSearch SQL': { + ...allowedSearchOperations['OpenSearch SQL'], + language: 'OpenSearch SQL', + dataset: INDEX_PATTERN_WITH_TIME, + queryString: `SELECT * FROM ${INDEX_PATTERN_WITH_TIME} WHERE bytes_transferred > 9950`, + hitCount: undefined, + sampleTableData: [], + saveName: 'sql-index-pattern-01', + }, + PPL: { + ...allowedSearchOperations.PPL, + language: 'PPL', + dataset: INDEX_PATTERN_WITH_TIME, + queryString: `source = ${INDEX_PATTERN_WITH_TIME} | where bytes_transferred > 9950`, + hitCount: 101, + sampleTableData: [], + saveName: 'ppl-index-pattern-01', + }, +}; +const indexTestConfigurations = { + 'OpenSearch SQL': { + ...allowedSearchOperations['OpenSearch SQL'], + language: 'OpenSearch SQL', + dataset: INDEX_WITH_TIME_1, + queryString: `SELECT * FROM ${INDEX_WITH_TIME_1} WHERE bytes_transferred > 9950`, + hitCount: undefined, + sampleTableData: [], + saveName: 'sql-index-01', + }, + PPL: { + ...allowedSearchOperations.PPL, + language: 'PPL', + dataset: INDEX_WITH_TIME_1, + queryString: `source = ${INDEX_WITH_TIME_1} | where bytes_transferred > 9950`, + hitCount: 50, + sampleTableData: [], + saveName: 'ppl-index-01', + }, +}; +const allTestConfigurations = [ + ...Object.values(indexPatternTestConfigurations), + ...Object.values(indexTestConfigurations), +]; + +// Maps a dataset that are used in this file to the exact string that dataset +// corresponds to in the saved search API response +const mapDatasetToType = (dataset) => { + switch (dataset) { + case INDEX_PATTERN_WITH_TIME: + return 'INDEX_PATTERN'; + case INDEX_WITH_TIME_1: + return 'INDEXES'; + default: + return 'unknown dataset'; + } +}; + +// Maps a language that are used in this file to the exact string that the language +// corresponds to in the saved search API response +const mapLanguageToApiResponseString = (language) => { + switch (language) { + case 'DQL': + return 'kuery'; + case 'Lucene': + return 'lucene'; + case 'OpenSearch SQL': + return 'SQL'; + case 'PPL': + return 'PPL'; + default: + return 'unknown language'; + } +}; + +// Escapes special characters for the editor +const prepareQueryStringForEditor = (queryString) => { + return queryString.replaceAll(/([{}])/g, (char) => `{${char}}`); +}; + +// In theory this function is not needed as we should be able to use +// the custom command navigateToWorkSpaceSpecificPage, but when using that +// to go to the discover page, it sometimes leads to a blank page +const navigateToDiscoverPage = () => { + cy.navigateToWorkSpaceHomePage(workspaceName); + cy.getElementByTestId('headerAppActionMenu').should('be.visible'); +}; + +// Sets the dataset for the search. Since INDEXES are not saved to the dataset, +// we need to click through various buttons to manually add them +const setDataset = (dataset) => { + const datasetType = mapDatasetToType(dataset); + cy.getElementByTestId('datasetSelectorButton').should('be.visible').click(); + + if (datasetType === 'INDEX_PATTERN') { + cy.get(`[title="${dataset}"]`).click(); + } else if (datasetType === 'INDEXES') { + cy.getElementByTestId('datasetSelectorAdvancedButton').click(); + cy.contains('span', 'Indexes').click(); + cy.contains('span', datasourceName).click(); + // this element is sometimes being masked by another element + cy.contains('span', dataset).should('be.visible').click({ force: true }); + cy.getElementByTestId('datasetSelectorNext').click(); + + cy.getElementByTestId('advancedSelectorTimeFieldSelect').select('timestamp'); + cy.getElementByTestId('advancedSelectorConfirmButton').click(); + + cy.getElementByTestId('datasetSelectorButton').should( + 'contain.text', + `${datasourceName}::${dataset}` + ); + } +}; + +const setDatePickerDatesAndSearchIfRelevant = (language) => { + if (language === 'OpenSearch SQL') { + return; + } + + cy.setTopNavDate(START_TIME, END_TIME); +}; + +const setSearchConfigurations = ({ + addFilter, + queryString, + setHistogramInterval, + selectFields, + applySort, +}) => { + if (addFilter) { + cy.getElementByTestId('showFilterActions').click(); + cy.getElementByTestId('addFilters').click(); + cy.getElementByTestId('filterFieldSuggestionList').find('input').type('category'); + cy.getElementByTestId('comboBoxOptionsList filterFieldSuggestionList-optionsList') + .find('button[title="category"]') + .click(); + cy.getElementByTestId('filterOperatorList').find('input').type('is one of'); + cy.getElementByTestId('comboBoxOptionsList filterOperatorList-optionsList') + .find('button[title="is one of"]') + .click(); + cy.getElementByTestId('filterParams').find('input').type('Application'); + cy.getElementByTestId( + 'comboBoxOptionsList filterParamsComboBox phrasesParamsComboxBox-optionsList' + ) + .find('button[title="Application"]') + .click(); + // Need to wait here a bit to avoid cypress flakiness + cy.wait(750); + cy.get('span[title="Application"]').should('be.visible'); + // Need to wait here a bit to avoid cypress error + cy.wait(750); + // force is true below because sometimes a dropdown covers the button + cy.getElementByTestId('saveFilter').click({ force: true }); + } + + cy.setQueryEditor(queryString); + + if (setHistogramInterval) { + cy.getElementByTestId('discoverIntervalSelect').select('w'); + } + + if (selectFields) { + cy.getElementByTestId('fieldToggle-bytes_transferred').click(); + cy.getElementByTestId('fieldToggle-personal.name').click(); + + // reloading as field filtering doesn't appear right away on cypress. Issue only appears in cypress tests, + // so resolving it via a force reload. + // cy.reload(); + cy.getElementByTestId('querySubmitButton').should('be.visible'); + } + + if (applySort) { + cy.getElementByTestId('docTableHeaderFieldSort_bytes_transferred').click(); + + // stop sorting based on timestamp + cy.getElementByTestId('docTableHeaderFieldSort_timestamp').click(); + cy.getElementByTestId('docTableHeaderFieldSort_timestamp').trigger('mouseover'); + cy.contains('div', 'Sort timestamp ascending').should('be.visible'); + + cy.getElementByTestId('docTableHeaderFieldSort_bytes_transferred').click(); + + // TODO: This reload shouldn't need to be here, but currently the sort doesn't always happen right away + cy.reload(); + cy.getElementByTestId('querySubmitButton').should('be.visible'); + } +}; + +const verifyDiscoverPageState = ({ + dataset, + queryString, + language, + hitCount, + checkFilter, + checkHistogramInterval, + checkSelectedField, + verifyTableData = [], +}) => { + cy.getElementByTestId('datasetSelectorButton').contains(dataset); + if (language === 'OpenSearch SQL' || language === 'PPL') { + cy.getElementByTestId('osdQueryEditor__multiLine').contains(queryString); + } else { + cy.getElementByTestId('osdQueryEditor__singleLine').contains(queryString); + } + cy.getElementByTestId('queryEditorLanguageSelector').contains(language); + + if (checkFilter) { + cy.getElementByTestId( + 'filter filter-enabled filter-key-category filter-value-Application filter-unpinned ' + ).should('exist'); + } + if (hitCount) { + cy.verifyHitCount(hitCount); + } + + if (checkHistogramInterval) { + // TODO: Uncomment this once bug is fixed, currently the interval is not saving + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9077 + // cy.getElementByTestId('discoverIntervalSelect').should('have.value', 'w'); + } + + if (checkSelectedField) { + cy.getElementByTestId('docTableHeaderField').should('have.length', 3); + cy.getElementByTestId('docTableHeader-timestamp').should('be.visible'); + cy.getElementByTestId('docTableHeader-bytes_transferred').should('be.visible'); + cy.getElementByTestId('docTableHeader-personal.name').should('be.visible'); + } + // verify first row to ensure sorting is working, but ignore the timestamp field as testing environment might have differing timezones + verifyTableData.forEach(([index, value]) => { + cy.getElementByTestId('osdDocTableCellDataField').eq(index).contains(value); + }); +}; + +// if searchName is not passed, assume they want to save as new search +const saveSearch = ({ searchName, saveAsNew }) => { + cy.getElementByTestId('discoverSaveButton').click(); + if (searchName) { + cy.getElementByTestId('savedObjectTitle').type(searchName); + } + + if (saveAsNew) { + cy.getElementByTestId('saveAsNewCheckbox').click(); + } + cy.getElementByTestId('confirmSaveSavedObjectButton').click(); + + // if saving as new save search, you need to click confirm twice; + if (saveAsNew) { + cy.getElementByTestId('confirmSaveSavedObjectButton').click(); + } + cy.getElementByTestId('euiToastHeader').contains(/was saved/); +}; + +const verifySavedSearchInAssetsPage = ({ + dataset, + searchName, + queryString, + language, + checkHistogramInterval, + checkSelectedField, + checkSort, + checkFilter, +}) => { + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'objects', + isEnhancement: true, + }); + + // TODO: Currently this test will only work if the last saved object is the relevant savedSearch + // Update below to make it work without that requirement. + cy.getElementByTestId('euiCollapsedItemActionsButton').last().click(); + + cy.intercept('POST', '/w/*/api/saved_objects/_bulk_get').as('savedObjectResponse'); + cy.getElementByTestId('savedObjectsTableAction-inspect').click(); + + cy.wait('@savedObjectResponse').then((interception) => { + const savedObjectAttributes = interception.response.body.saved_objects[0].attributes; + const searchSource = savedObjectAttributes.kibanaSavedObjectMeta.searchSourceJSON; + + expect(savedObjectAttributes.title).eq(searchName); + if (checkSelectedField) { + expect(savedObjectAttributes.columns).eqls(['bytes_transferred', 'personal.name']); + } + if (checkSort) { + expect(savedObjectAttributes.sort).eqls([['bytes_transferred', 'desc']]); + } + expect(searchSource).match( + // all special characters must be escaped + new RegExp(`"query":"${queryString.replaceAll(/([*{}])/g, (char) => `\\${char}`)}"`) + ); + expect(searchSource).match( + new RegExp(`"language":"${mapLanguageToApiResponseString(language)}"`) + ); + expect(searchSource).match(new RegExp(`"title":"${dataset.replace('*', '\\*')}"`)); + expect(searchSource).match(new RegExp(`"type":"${mapDatasetToType(dataset)}"`)); + + if (checkHistogramInterval) { + expect(searchSource).match(/"calendar_interval":"1w"/); + } + if (checkFilter) { + expect(searchSource).match(/"match_phrase":\{"category":"Application"\}/); + } + }); +}; + +const loadSavedSearch = (searchName, selectDuplicate = false) => { + cy.getElementByTestId('discoverOpenButton').click(); + if (selectDuplicate) { + cy.getElementByTestId(`savedObjectTitle${searchName}`).last().click(); + } else { + cy.getElementByTestId(`savedObjectTitle${searchName}`).first().click(); + } + + cy.get('h1').contains(searchName).should('be.visible'); +}; + +const setSearchAndSaveAndVerify = (config) => { + navigateToDiscoverPage(); + setDataset(config.dataset); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + + setSearchConfigurations({ + addFilter: config.filters, + queryString: prepareQueryStringForEditor(config.queryString, config.language, config.dataset), + setHistogramInterval: config.histogram, + selectFields: config.selectFields, + applySort: config.sort, + }); + verifyDiscoverPageState({ + dataset: config.dataset, + queryString: config.queryString, + language: config.language, + hitCount: config.hitCount, + checkFilter: config.filters, + checkHistogramInterval: config.histogram, + checkSelectedField: config.selectFields, + verifyTableData: config.sampleTableData, + }); + saveSearch({ searchName: config.saveName }); + + // There is a small chance where if we go to assets page, + // the saved search does not appear. So adding this wait + cy.wait(1000); + + verifySavedSearchInAssetsPage({ + dataset: config.dataset, + searchName: config.saveName, + queryString: config.queryString, + language: config.language, + checkHistogramInterval: config.histogram, + checkSelectedField: config.selectFields, + checkSort: config.sort, + checkFilter: config.filters, + }); +}; + +const loadSavedSearchAndVerify = (config) => { + // We are starting from various languages + // to guard against: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9078 + ['DQL', 'Lucene', 'OpenSearch SQL', 'PPL'].forEach((startingLanguage) => { + // TODO: Remove this line once bugs are fixed + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9078 + if (startingLanguage !== config.language) return; + + navigateToDiscoverPage(); + cy.getElementByTestId('discoverNewButton').click(); + + // Intentionally setting INDEX_PATTERN dataset here so that + // we have access to all four languages that INDEX_PATTERN allows. + // This means that we are only testing loading a saved search + // starting from an INDEX_PATTERN dataset, but I think testing where the + // start is a permutation of other dataset is overkill + setDataset(INDEX_PATTERN_WITH_TIME); + + cy.setQueryLanguage(startingLanguage); + loadSavedSearch(config.saveName); + setDatePickerDatesAndSearchIfRelevant(config.language); + verifyDiscoverPageState({ + dataset: config.dataset, + queryString: config.queryString, + language: config.language, + hitCount: config.hitCount, + checkFilter: config.filters, + checkHistogramInterval: config.histogram, + checkSelectedField: config.selectFields, + verifyTableData: config.sampleTableData, + }); + }); +}; + +export const runSavedSearchCreateTests = () => { + describe('saved search', () => { + beforeEach(() => { + // Load test data + cy.setupTestData( + SECONDARY_ENGINE.url, + [ + `cypress/fixtures/query_enhancements/data-logs-1/${INDEX_WITH_TIME_1}.mapping.json`, + `cypress/fixtures/query_enhancements/data-logs-2/${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-2/${INDEX_WITH_TIME_2}.data.ndjson`, + ] + ); + // Add data source + cy.addDataSource({ + name: datasourceName, + url: SECONDARY_ENGINE.url, + authType: 'no_auth', + }); + // Create workspace + cy.deleteWorkspaceByName(workspaceName); + cy.visit('/app/home'); + cy.createInitialWorkspaceWithDataSource(datasourceName, workspaceName); + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_PATTERN_WITH_TIME.replace('*', ''), + timefieldName: 'timestamp', + isEnhancement: true, + }); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(workspaceName); + // // TODO: Modify deleteIndex to handle an array of index and remove hard code + cy.deleteDataSourceByName(datasourceName); + cy.deleteIndex(INDEX_WITH_TIME_1); + cy.deleteIndex(INDEX_WITH_TIME_2); + }); + + allTestConfigurations.forEach((config) => { + it(`should successfully create a saved search and load it for ${mapDatasetToType( + config.dataset + ).toLowerCase()} ${config.language}`, () => { + setSearchAndSaveAndVerify(config); + loadSavedSearchAndVerify(config); + }); + }); + }); +}; + +runSavedSearchCreateTests(); diff --git a/cypress/utils/apps/query_enhancements/commands.js b/cypress/utils/apps/query_enhancements/commands.js index 654f3f8fc5e..7193286fe92 100644 --- a/cypress/utils/apps/query_enhancements/commands.js +++ b/cypress/utils/apps/query_enhancements/commands.js @@ -3,16 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -Cypress.Commands.add('setSingleLineQueryEditor', (value, submit = true) => { +Cypress.Commands.add('setQueryEditor', (value, submit = true) => { const opts = { log: false }; Cypress.log({ - name: 'setSingleLineQueryEditor', + name: 'setQueryEditor', displayName: 'set query', message: value, }); - cy.getElementByTestId('osdQueryEditor__singleLine', opts).type(value, opts); + // 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); if (submit) { cy.updateTopNav(opts); diff --git a/cypress/utils/apps/query_enhancements/index.d.ts b/cypress/utils/apps/query_enhancements/index.d.ts index 05caf8d7b2a..5955e524417 100644 --- a/cypress/utils/apps/query_enhancements/index.d.ts +++ b/cypress/utils/apps/query_enhancements/index.d.ts @@ -5,7 +5,7 @@ declare namespace Cypress { interface Chainable { - setSingleLineQueryEditor(value: string, submit?: boolean): Chainable; + setQueryEditor(value: string, submit?: boolean): Chainable; setQueryLanguage(value: 'DQL' | 'Lucene' | 'OpenSearch SQL' | 'PPL'): Chainable; addDataSource(opts: { name: string; diff --git a/cypress/utils/dashboards/commands.js b/cypress/utils/dashboards/commands.js index e6696637f63..d26fd09aeaf 100644 --- a/cypress/utils/dashboards/commands.js +++ b/cypress/utils/dashboards/commands.js @@ -49,9 +49,9 @@ Cypress.Commands.add( Cypress.Commands.add( // navigates to the workspace HomePage of a given workspace 'navigateToWorkSpaceHomePage', - (url, workspaceName) => { + (workspaceName) => { // Selecting the correct workspace - cy.visit(`${url}/app/workspace_list#`); + cy.visit('/app/workspace_list#'); cy.openWorkspaceDashboard(workspaceName); } ); @@ -60,9 +60,9 @@ Cypress.Commands.add( //navigate to workspace specific pages 'navigateToWorkSpaceSpecificPage', (opts) => { - const { url, workspaceName, page, isEnhancement = false } = opts; + const { workspaceName, page, isEnhancement = false } = opts; // Navigating to the WorkSpace Home Page - cy.navigateToWorkSpaceHomePage(url, workspaceName); + cy.navigateToWorkSpaceHomePage(workspaceName); cy.waitForLoader(isEnhancement); // Check for toggleNavButton and handle accordingly @@ -88,7 +88,6 @@ Cypress.Commands.add( 'createWorkspaceIndexPatterns', (opts) => { const { - url, workspaceName, indexPattern, timefieldName, @@ -99,7 +98,6 @@ Cypress.Commands.add( // Navigate to Workspace Specific IndexPattern Page cy.navigateToWorkSpaceSpecificPage({ - url, workspaceName, page: 'indexPatterns', isEnhancement, @@ -147,11 +145,10 @@ Cypress.Commands.add( // Don't use * in the indexPattern it adds it by default at the end of name 'deleteWorkspaceIndexPatterns', (opts) => { - const { url, workspaceName, indexPattern, isEnhancement = false } = opts; + const { workspaceName, indexPattern, isEnhancement = false } = opts; // Navigate to Workspace Specific IndexPattern Page cy.navigateToWorkSpaceSpecificPage({ - url, workspaceName, page: 'indexPatterns', isEnhancement, diff --git a/cypress/utils/dashboards/index.d.ts b/cypress/utils/dashboards/index.d.ts index 78a534c3bc9..146a59acb77 100644 --- a/cypress/utils/dashboards/index.d.ts +++ b/cypress/utils/dashboards/index.d.ts @@ -5,15 +5,13 @@ declare namespace Cypress { interface Chainable { - navigateToWorkSpaceHomePage(url: string, workspaceName: string): Chainable; + navigateToWorkSpaceHomePage(workspaceName: string): Chainable; navigateToWorkSpaceSpecificPage(opts: { - url: string; workspaceName: string; page: string; isEnhancement?: boolean; }): Chainable; createWorkspaceIndexPatterns(opts: { - url: string; workspaceName: string; indexPattern: string; timefieldName?: string; @@ -22,7 +20,6 @@ declare namespace Cypress { isEnhancement?: boolean; }): Chainable; deleteWorkspaceIndexPatterns(opts: { - url: string; workspaceName: string; indexPattern: string; isEnhancement?: boolean; From ab72481f673de137bc003d4c43645f782f7b5602 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:25:08 +0000 Subject: [PATCH 2/5] Changeset file for PR #9112 created/updated --- changelogs/fragments/9112.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/9112.yml diff --git a/changelogs/fragments/9112.yml b/changelogs/fragments/9112.yml new file mode 100644 index 00000000000..dbeadb7b928 --- /dev/null +++ b/changelogs/fragments/9112.yml @@ -0,0 +1,2 @@ +test: +- Add tests for saving search and loading it ([#9112](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9112)) \ No newline at end of file From fef7bba7f0b95a69919a16d15229825174581382 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Fri, 20 Dec 2024 16:52:38 -0800 Subject: [PATCH 3/5] remove unused imports Signed-off-by: Justin Kim --- .../apps/query_enhancements/dataset_selector.spec.js | 2 +- .../apps/query_enhancements/queries.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js index f5fd480e7a4..e4356c84d34 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js @@ -4,7 +4,7 @@ */ import { WORKSPACE_NAME, DATASOURCE_NAME, START_TIME, END_TIME } from './constants'; -import { BASE_PATH, SECONDARY_ENGINE } from '../../../../../utils/constants'; +import { SECONDARY_ENGINE } from '../../../../../utils/constants'; describe('dataset selector', { scrollBehavior: false }, () => { before(() => { diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js index 882ebb3ba3c..cd482a32fdb 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js @@ -4,7 +4,7 @@ */ import { WORKSPACE_NAME, DATASOURCE_NAME, START_TIME, END_TIME } from './constants'; -import { BASE_PATH, SECONDARY_ENGINE } from '../../../../../utils/constants'; +import { SECONDARY_ENGINE } from '../../../../../utils/constants'; describe('query enhancement queries', { scrollBehavior: false }, () => { before(() => { From d2b3a0d9b2f1911dc420c412b7a47c31bb35f5d3 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Mon, 23 Dec 2024 09:59:19 -0800 Subject: [PATCH 4/5] click on a random element before typing on query editor to bypass the popover that appears Signed-off-by: Justin Kim --- cypress/utils/apps/query_enhancements/commands.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cypress/utils/apps/query_enhancements/commands.js b/cypress/utils/apps/query_enhancements/commands.js index 7193286fe92..4b1714b4440 100644 --- a/cypress/utils/apps/query_enhancements/commands.js +++ b/cypress/utils/apps/query_enhancements/commands.js @@ -12,6 +12,10 @@ Cypress.Commands.add('setQueryEditor', (value, submit = true) => { message: value, }); + // On a new session, a syntax helper popover appears, which obstructs the typing within the query + // editor. Clicking on a random element removes the popover. + cy.getElementByTestId('headerGlobalNav').click(); + // clear the editor first and then set cy.get('.globalQueryEditor .react-monaco-editor-container') .click() From 36c75d9d1ba9e55f6b3ed8ea060d3260aa78cf64 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 26 Dec 2024 20:28:49 -0800 Subject: [PATCH 5/5] address pr comments Signed-off-by: Justin Kim --- .../apps/query_enhancements/constants.js | 69 +- .../dataset_selector.spec.js | 40 +- .../query_enhancements/saved_search.spec.js | 652 +++++++++--------- cypress/utils/apps/data_explorer/commands.js | 82 ++- cypress/utils/apps/data_explorer/index.d.ts | 11 +- cypress/utils/apps/index.d.ts | 2 +- .../utils/apps/query_enhancements/commands.js | 55 +- .../utils/apps/query_enhancements/index.d.ts | 17 +- cypress/utils/dashboards/commands.js | 4 +- 9 files changed, 523 insertions(+), 409 deletions(-) diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js index 07612aba16e..c06f1097dac 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/constants.js @@ -4,10 +4,77 @@ */ export const DATASOURCE_NAME = 'query-cluster'; -export const WORKSPACE_NAME = 'query-workspace'; +export const WORKSPACE_NAME = 'query-ws'; export const START_TIME = 'Jan 1, 2020 @ 00:00:00.000'; export const END_TIME = 'Jan 1, 2024 @ 00:00:00.000'; export const INDEX_WITH_TIME_1 = 'data_logs_small_time_1'; export const INDEX_WITH_TIME_2 = 'data_logs_small_time_2'; export const INDEX_PATTERN_WITH_TIME = 'data_logs_small_time_*'; + +// Maps all the query languages that is supported by query enhancements. +// name: Name of the language as it appears in the dashboard app +// apiName: Name of the language recognized by the OpenSearch API +// supports: list of search operations that are viable for the given language +export const QueryLanguages = { + DQL: { + name: 'DQL', + apiName: 'kuery', + supports: { + filters: true, + histogram: true, + selectFields: true, + sort: true, + }, + }, + Lucene: { + name: 'Lucene', + apiName: 'lucene', + supports: { + filters: true, + histogram: true, + selectFields: true, + sort: true, + }, + }, + SQL: { + name: 'OpenSearch SQL', + apiName: 'SQL', + supports: { + filters: false, + histogram: false, + selectFields: true, + sort: false, + }, + }, + PPL: { + name: 'PPL', + apiName: 'PPL', + supports: { + filters: false, + // TODO: Set this to true once 2.17 is updated to include histogram + histogram: true, + selectFields: true, + sort: false, + }, + }, +}; + +// Maps the dataset types that are supported by query enhancements. +// name: Name of the dataset as recognized by the OpenSearch API +// supportedLanguages: List of all the languages that the dataset supports +export const DatasetTypes = { + INDEX_PATTERN: { + name: 'INDEX_PATTERN', + supportedLanguages: [ + QueryLanguages.DQL, + QueryLanguages.Lucene, + QueryLanguages.SQL, + QueryLanguages.PPL, + ], + }, + INDEXES: { + name: 'INDEXES', + supportedLanguages: [QueryLanguages.SQL, QueryLanguages.PPL], + }, +}; diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js index e4356c84d34..93732a71abc 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/dataset_selector.spec.js @@ -38,21 +38,7 @@ describe('dataset selector', { scrollBehavior: false }, () => { describe('select indices', () => { it('with SQL as default language', function () { - cy.getElementByTestId(`datasetSelectorButton`).click(); - cy.getElementByTestId(`datasetSelectorAdvancedButton`).click(); - cy.get(`[title="Indexes"]`).click(); - cy.get(`[title=${DATASOURCE_NAME}]`).click(); - cy.get(`[title="data_logs_small_time_1"]`).click(); // Updated to match loaded data - cy.getElementByTestId('datasetSelectorNext').click(); - - cy.get(`[class="euiModalHeader__title"]`).should('contain', 'Step 2: Configure data'); - - //select SQL - cy.getElementByTestId('advancedSelectorLanguageSelect').select('OpenSearch SQL'); - cy.getElementByTestId(`advancedSelectorTimeFieldSelect`).select('timestamp'); - cy.getElementByTestId('advancedSelectorConfirmButton').click(); - - cy.waitForLoader(true); + cy.setIndexAsDataset('data_logs_small_time_1', DATASOURCE_NAME, 'OpenSearch SQL'); // SQL should already be selected cy.getElementByTestId('queryEditorLanguageSelector').should('contain', 'OpenSearch SQL'); @@ -70,22 +56,7 @@ describe('dataset selector', { scrollBehavior: false }, () => { }); it('with PPL as default language', function () { - cy.getElementByTestId(`datasetSelectorButton`).click(); - cy.getElementByTestId(`datasetSelectorAdvancedButton`).click(); - cy.get(`[title="Indexes"]`).click(); - cy.get(`[title=${DATASOURCE_NAME}]`).click(); - cy.get(`[title="data_logs_small_time_1"]`).click(); // Updated to match loaded data - cy.getElementByTestId('datasetSelectorNext').click(); - - cy.get(`[class="euiModalHeader__title"]`).should('contain', 'Step 2: Configure data'); - - //select PPL - cy.getElementByTestId('advancedSelectorLanguageSelect').select('PPL'); - - cy.getElementByTestId(`advancedSelectorTimeFieldSelect`).select('timestamp'); - cy.getElementByTestId('advancedSelectorConfirmButton').click(); - - cy.waitForLoader(true); + cy.setIndexAsDataset('data_logs_small_time_1', DATASOURCE_NAME, 'PPL'); // PPL should already be selected cy.getElementByTestId('queryEditorLanguageSelector').should('contain', 'PPL'); @@ -122,10 +93,9 @@ describe('dataset selector', { scrollBehavior: false }, () => { cy.navigateToWorkSpaceHomePage(WORKSPACE_NAME); cy.waitForLoader(true); - cy.getElementByTestId(`datasetSelectorButton`).click(); - cy.getElementByTestId(`datasetSelectorAdvancedButton`).click(); - cy.get(`[title="Index Patterns"]`).click(); - cy.get(`[title="${DATASOURCE_NAME}::data_logs_small_time_1*"]`).should('exist'); + cy.setIndexPatternAsDataset('data_logs_small_time_1*', DATASOURCE_NAME); + // setting OpenSearch SQL as the code following it does not work if this test is isolated + cy.setQueryLanguage('OpenSearch SQL'); cy.waitForLoader(true); cy.waitForSearch(); diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js index 5fcdb3040f8..a8fac026b9a 100644 --- a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js @@ -4,252 +4,257 @@ */ import { + DATASOURCE_NAME, END_TIME, START_TIME, INDEX_PATTERN_WITH_TIME, INDEX_WITH_TIME_1, INDEX_WITH_TIME_2, + WORKSPACE_NAME, + DatasetTypes, + QueryLanguages, } from './constants'; import { SECONDARY_ENGINE } from '../../../../../utils/constants'; import { v4 as uuid } from 'uuid'; -const workspaceName = uuid(); +const workspaceName = `${WORKSPACE_NAME}-${uuid().substring(0, 9)}`; // datasource name must be 32 char or less -const datasourceName = uuid().substring(0, 32); - -const allowedSearchOperations = { - DQL: { - filters: true, - histogram: true, - selectFields: true, - sort: true, - }, - Lucene: { - filters: true, - histogram: true, - selectFields: true, - sort: true, - }, - 'OpenSearch SQL': { - filters: false, - histogram: false, - selectFields: true, - sort: false, - }, - PPL: { - filters: false, - histogram: true, - selectFields: true, - sort: false, - }, +const datasourceName = `${DATASOURCE_NAME}-${uuid().substring(0, 18)}`; +let workspaceId = ''; +let datasourceId = ''; +let indexPatternId = ''; + +const SELECTED_FIELD_COLUMNS = ['bytes_transferred', 'personal.name']; +const APPLIED_SORT = [['bytes_transferred', 'desc']]; +const APPLIED_FILTERS = { + field: 'category', + operator: 'is one of', + value: 'Application', }; -const indexPatternTestConfigurations = { - DQL: { - ...allowedSearchOperations.DQL, - language: 'DQL', - dataset: INDEX_PATTERN_WITH_TIME, - queryString: 'bytes_transferred > 9950', - hitCount: 28, - sampleTableData: [ - [1, '9,998'], - [2, 'Phyllis Dach'], +// Returns the body that is needed when creating a saved search directly through API call +const getSavedObjectPostBody = (config) => { + return { + attributes: { + title: config.saveName, + description: '', + hits: 0, + columns: config.selectFields ? SELECTED_FIELD_COLUMNS : undefined, + sort: config.sort ? APPLIED_SORT : undefined, + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: `{"query":{"query":"${config.queryString}","language":"${ + config.apiLanguage + }","dataset":${`{"id":"${ + config.datasetType === DatasetTypes.INDEX_PATTERN.name + ? indexPatternId + : `${datasourceId}::${config.dataset}` + }","timeFieldName":"timestamp","title":"${config.dataset}","type":"${ + config.datasetType + }"}`}},"highlightAll":true,"version":true,"aggs":{"2":{"date_histogram":{"field":"timestamp","calendar_interval":"1w","time_zone":"America/Los_Angeles","min_doc_count":1}}},"filter":[{"$state":{"store":"appState"},"meta":{"alias":null,"disabled":false,"key":"${ + APPLIED_FILTERS.field + }","negate":false,"params":["${APPLIED_FILTERS.value}"],"type":"phrases","value":"${ + APPLIED_FILTERS.value + }","indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"},"query":{"bool":{"minimum_should_match":1,"should":[{"match_phrase":{"${ + APPLIED_FILTERS.field + }":"${ + APPLIED_FILTERS.value + }"}}]}}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}`, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + id: indexPatternId, + }, ], - saveName: 'dql-index-pattern-01', - }, - Lucene: { - ...allowedSearchOperations.Lucene, - language: 'Lucene', - dataset: INDEX_PATTERN_WITH_TIME, - queryString: 'bytes_transferred: {9950 TO *}', - hitCount: 28, - sampleTableData: [ - [1, '9,998'], - [2, 'Phyllis Dach'], - ], - saveName: 'lucene-index-pattern-01', - }, - 'OpenSearch SQL': { - ...allowedSearchOperations['OpenSearch SQL'], - language: 'OpenSearch SQL', - dataset: INDEX_PATTERN_WITH_TIME, - queryString: `SELECT * FROM ${INDEX_PATTERN_WITH_TIME} WHERE bytes_transferred > 9950`, - hitCount: undefined, - sampleTableData: [], - saveName: 'sql-index-pattern-01', - }, - PPL: { - ...allowedSearchOperations.PPL, - language: 'PPL', - dataset: INDEX_PATTERN_WITH_TIME, - queryString: `source = ${INDEX_PATTERN_WITH_TIME} | where bytes_transferred > 9950`, - hitCount: 101, - sampleTableData: [], - saveName: 'ppl-index-pattern-01', - }, -}; -const indexTestConfigurations = { - 'OpenSearch SQL': { - ...allowedSearchOperations['OpenSearch SQL'], - language: 'OpenSearch SQL', - dataset: INDEX_WITH_TIME_1, - queryString: `SELECT * FROM ${INDEX_WITH_TIME_1} WHERE bytes_transferred > 9950`, - hitCount: undefined, - sampleTableData: [], - saveName: 'sql-index-01', - }, - PPL: { - ...allowedSearchOperations.PPL, - language: 'PPL', - dataset: INDEX_WITH_TIME_1, - queryString: `source = ${INDEX_WITH_TIME_1} | where bytes_transferred > 9950`, - hitCount: 50, - sampleTableData: [], - saveName: 'ppl-index-01', - }, + workspaces: [workspaceId], + }; }; -const allTestConfigurations = [ - ...Object.values(indexPatternTestConfigurations), - ...Object.values(indexTestConfigurations), -]; - -// Maps a dataset that are used in this file to the exact string that dataset -// corresponds to in the saved search API response -const mapDatasetToType = (dataset) => { - switch (dataset) { - case INDEX_PATTERN_WITH_TIME: - return 'INDEX_PATTERN'; - case INDEX_WITH_TIME_1: - return 'INDEXES'; + +const getExpectedHitCount = (datasetType, language) => { + switch (datasetType) { + case DatasetTypes.INDEX_PATTERN.name: + switch (language) { + case QueryLanguages.DQL.name: + return 28; + case QueryLanguages.Lucene.name: + return 28; + case QueryLanguages.SQL.name: + return undefined; + case QueryLanguages.PPL.name: + // TODO: Update this to 101 once Histogram is supported on 2.17 + return undefined; + default: + throw new Error( + `getExpectedHitCount encountered unsupported language for ${datasetType}: ${language}` + ); + } + case DatasetTypes.INDEXES.name: + switch (language) { + case QueryLanguages.SQL.name: + return undefined; + case QueryLanguages.PPL.name: + // TODO: Update this to 50 once Histogram is supported on 2.17 + return undefined; + default: + throw new Error( + `getExpectedHitCount encountered unsupported language for ${datasetType}: ${language}` + ); + } default: - return 'unknown dataset'; + throw new Error(`getExpectedHitCount encountered unsupported datasetType: ${datasetType}`); } }; -// Maps a language that are used in this file to the exact string that the language -// corresponds to in the saved search API response -const mapLanguageToApiResponseString = (language) => { - switch (language) { - case 'DQL': - return 'kuery'; - case 'Lucene': - return 'lucene'; - case 'OpenSearch SQL': - return 'SQL'; - case 'PPL': - return 'PPL'; +// returns an array of data present in the results table to check against +// For each element in the outer array, the 0th index is the index of the table cell +// and the 1st index is the value that the cell should contain. +// We are testing the table data to ensure that sorting is working as expected +const getSampleTableData = (datasetType, language) => { + switch (datasetType) { + case DatasetTypes.INDEX_PATTERN.name: + switch (language) { + case QueryLanguages.DQL.name: + return [ + [1, '9,998'], + [2, 'Phyllis Dach'], + ]; + case QueryLanguages.Lucene.name: + return [ + [1, '9,998'], + [2, 'Phyllis Dach'], + ]; + case QueryLanguages.SQL.name: + return []; + case QueryLanguages.PPL.name: + return []; + default: + throw new Error( + `getSampleTableData encountered unsupported language for ${datasetType}: ${language}` + ); + } + case DatasetTypes.INDEXES.name: + switch (language) { + case QueryLanguages.SQL.name: + return []; + case QueryLanguages.PPL.name: + return []; + default: + throw new Error( + `getSampleTableData encountered unsupported language for ${datasetType}: ${language}` + ); + } default: - return 'unknown language'; + throw new Error(`getSampleTableData encountered unsupported datasetType: ${datasetType}`); } }; -// Escapes special characters for the editor -const prepareQueryStringForEditor = (queryString) => { - return queryString.replaceAll(/([{}])/g, (char) => `{${char}}`); +const getQueryString = (dataset, language) => { + switch (language) { + case QueryLanguages.DQL.name: + return 'bytes_transferred > 9950'; + case QueryLanguages.Lucene.name: + return 'bytes_transferred: {9950 TO *}'; + case QueryLanguages.SQL.name: + return `SELECT * FROM ${dataset} WHERE bytes_transferred > 9950`; + case QueryLanguages.PPL.name: + return `source = ${dataset} | where bytes_transferred > 9950`; + default: + throw new Error(`getQueryString encountered unsupported language: ${language}`); + } }; -// In theory this function is not needed as we should be able to use -// the custom command navigateToWorkSpaceSpecificPage, but when using that -// to go to the discover page, it sometimes leads to a blank page -const navigateToDiscoverPage = () => { - cy.navigateToWorkSpaceHomePage(workspaceName); - cy.getElementByTestId('headerAppActionMenu').should('be.visible'); +const generateTestConfiguration = (dataset, datasetType, language) => { + const baseConfig = { + dataset, + datasetType, + language: language.name, + apiLanguage: language.apiName, + saveName: `${language.name}-${datasetType}`, + testName: `${language.name}-${datasetType}`, + ...language.supports, + }; + + return { + ...baseConfig, + queryString: getQueryString(dataset, language.name), + hitCount: getExpectedHitCount(datasetType, language.name), + sampleTableData: getSampleTableData(datasetType, language.name), + }; }; -// Sets the dataset for the search. Since INDEXES are not saved to the dataset, -// we need to click through various buttons to manually add them -const setDataset = (dataset) => { - const datasetType = mapDatasetToType(dataset); - cy.getElementByTestId('datasetSelectorButton').should('be.visible').click(); - - if (datasetType === 'INDEX_PATTERN') { - cy.get(`[title="${dataset}"]`).click(); - } else if (datasetType === 'INDEXES') { - cy.getElementByTestId('datasetSelectorAdvancedButton').click(); - cy.contains('span', 'Indexes').click(); - cy.contains('span', datasourceName).click(); - // this element is sometimes being masked by another element - cy.contains('span', dataset).should('be.visible').click({ force: true }); - cy.getElementByTestId('datasetSelectorNext').click(); - - cy.getElementByTestId('advancedSelectorTimeFieldSelect').select('timestamp'); - cy.getElementByTestId('advancedSelectorConfirmButton').click(); - - cy.getElementByTestId('datasetSelectorButton').should( - 'contain.text', - `${datasourceName}::${dataset}` - ); - } +const generateAllTestConfigurations = () => { + return Object.values(DatasetTypes).flatMap((dataset) => + dataset.supportedLanguages.map((language) => { + let datasetToUse; + switch (dataset.name) { + case DatasetTypes.INDEX_PATTERN.name: + datasetToUse = INDEX_PATTERN_WITH_TIME; + break; + case DatasetTypes.INDEXES.name: + datasetToUse = INDEX_WITH_TIME_1; + break; + default: + throw new Error( + `generateAllTestConfigurations encountered unsupported dataset: ${dataset.name}` + ); + } + return generateTestConfiguration(datasetToUse, dataset.name, language); + }) + ); }; const setDatePickerDatesAndSearchIfRelevant = (language) => { - if (language === 'OpenSearch SQL') { + if (language === QueryLanguages.SQL.name) { return; } cy.setTopNavDate(START_TIME, END_TIME); }; -const setSearchConfigurations = ({ - addFilter, - queryString, - setHistogramInterval, - selectFields, - applySort, -}) => { - if (addFilter) { - cy.getElementByTestId('showFilterActions').click(); - cy.getElementByTestId('addFilters').click(); - cy.getElementByTestId('filterFieldSuggestionList').find('input').type('category'); - cy.getElementByTestId('comboBoxOptionsList filterFieldSuggestionList-optionsList') - .find('button[title="category"]') - .click(); - cy.getElementByTestId('filterOperatorList').find('input').type('is one of'); - cy.getElementByTestId('comboBoxOptionsList filterOperatorList-optionsList') - .find('button[title="is one of"]') - .click(); - cy.getElementByTestId('filterParams').find('input').type('Application'); - cy.getElementByTestId( - 'comboBoxOptionsList filterParamsComboBox phrasesParamsComboxBox-optionsList' - ) - .find('button[title="Application"]') - .click(); - // Need to wait here a bit to avoid cypress flakiness - cy.wait(750); - cy.get('span[title="Application"]').should('be.visible'); - // Need to wait here a bit to avoid cypress error - cy.wait(750); - // force is true below because sometimes a dropdown covers the button - cy.getElementByTestId('saveFilter').click({ force: true }); +const setSearchConfigurations = ({ filters, queryString, histogram, selectFields, sort }) => { + if (filters) { + cy.submitFilterFromDropDown( + APPLIED_FILTERS.field, + APPLIED_FILTERS.operator, + APPLIED_FILTERS.value, + true + ); } - cy.setQueryEditor(queryString); + cy.setQueryEditor(queryString, { parseSpecialCharSequences: false }); - if (setHistogramInterval) { + if (histogram) { cy.getElementByTestId('discoverIntervalSelect').select('w'); } if (selectFields) { - cy.getElementByTestId('fieldToggle-bytes_transferred').click(); - cy.getElementByTestId('fieldToggle-personal.name').click(); + for (const field of SELECTED_FIELD_COLUMNS) { + cy.getElementByTestId(`fieldToggle-${field}`).click(); + } - // reloading as field filtering doesn't appear right away on cypress. Issue only appears in cypress tests, - // so resolving it via a force reload. - // cy.reload(); cy.getElementByTestId('querySubmitButton').should('be.visible'); } - if (applySort) { - cy.getElementByTestId('docTableHeaderFieldSort_bytes_transferred').click(); + if (sort) { + cy.getElementByTestId(`docTableHeaderFieldSort_${APPLIED_SORT[0][0]}`).click(); // stop sorting based on timestamp cy.getElementByTestId('docTableHeaderFieldSort_timestamp').click(); cy.getElementByTestId('docTableHeaderFieldSort_timestamp').trigger('mouseover'); cy.contains('div', 'Sort timestamp ascending').should('be.visible'); - cy.getElementByTestId('docTableHeaderFieldSort_bytes_transferred').click(); + cy.getElementByTestId(`docTableHeaderFieldSort_${APPLIED_SORT[0][0]}`).click(); // TODO: This reload shouldn't need to be here, but currently the sort doesn't always happen right away + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9131 cy.reload(); cy.getElementByTestId('querySubmitButton').should('be.visible'); } @@ -260,74 +265,58 @@ const verifyDiscoverPageState = ({ queryString, language, hitCount, - checkFilter, - checkHistogramInterval, - checkSelectedField, - verifyTableData = [], + filters, + histogram, + selectFields, + sampleTableData = [], }) => { cy.getElementByTestId('datasetSelectorButton').contains(dataset); - if (language === 'OpenSearch SQL' || language === 'PPL') { + if ([QueryLanguages.SQL.name, QueryLanguages.PPL.name].includes(language)) { cy.getElementByTestId('osdQueryEditor__multiLine').contains(queryString); } else { cy.getElementByTestId('osdQueryEditor__singleLine').contains(queryString); } cy.getElementByTestId('queryEditorLanguageSelector').contains(language); - if (checkFilter) { + if (filters) { cy.getElementByTestId( - 'filter filter-enabled filter-key-category filter-value-Application filter-unpinned ' + `filter filter-enabled filter-key-${APPLIED_FILTERS.field} filter-value-${APPLIED_FILTERS.value} filter-unpinned ` ).should('exist'); } if (hitCount) { cy.verifyHitCount(hitCount); } - if (checkHistogramInterval) { + if (histogram) { // TODO: Uncomment this once bug is fixed, currently the interval is not saving // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9077 // cy.getElementByTestId('discoverIntervalSelect').should('have.value', 'w'); } - if (checkSelectedField) { + if (selectFields) { cy.getElementByTestId('docTableHeaderField').should('have.length', 3); cy.getElementByTestId('docTableHeader-timestamp').should('be.visible'); - cy.getElementByTestId('docTableHeader-bytes_transferred').should('be.visible'); - cy.getElementByTestId('docTableHeader-personal.name').should('be.visible'); + for (const field of SELECTED_FIELD_COLUMNS) { + cy.getElementByTestId(`docTableHeader-${field}`).should('be.visible'); + cy.getElementByTestId(`docTableHeader-${field}`).should('be.visible'); + } } // verify first row to ensure sorting is working, but ignore the timestamp field as testing environment might have differing timezones - verifyTableData.forEach(([index, value]) => { + sampleTableData.forEach(([index, value]) => { cy.getElementByTestId('osdDocTableCellDataField').eq(index).contains(value); }); }; -// if searchName is not passed, assume they want to save as new search -const saveSearch = ({ searchName, saveAsNew }) => { - cy.getElementByTestId('discoverSaveButton').click(); - if (searchName) { - cy.getElementByTestId('savedObjectTitle').type(searchName); - } - - if (saveAsNew) { - cy.getElementByTestId('saveAsNewCheckbox').click(); - } - cy.getElementByTestId('confirmSaveSavedObjectButton').click(); - - // if saving as new save search, you need to click confirm twice; - if (saveAsNew) { - cy.getElementByTestId('confirmSaveSavedObjectButton').click(); - } - cy.getElementByTestId('euiToastHeader').contains(/was saved/); -}; - const verifySavedSearchInAssetsPage = ({ + apiLanguage, dataset, - searchName, + saveName, queryString, - language, - checkHistogramInterval, - checkSelectedField, - checkSort, - checkFilter, + datasetType, + histogram, + selectFields, + sort, + filters, }) => { cy.navigateToWorkSpaceSpecificPage({ workspaceName: workspaceName, @@ -346,119 +335,33 @@ const verifySavedSearchInAssetsPage = ({ const savedObjectAttributes = interception.response.body.saved_objects[0].attributes; const searchSource = savedObjectAttributes.kibanaSavedObjectMeta.searchSourceJSON; - expect(savedObjectAttributes.title).eq(searchName); - if (checkSelectedField) { - expect(savedObjectAttributes.columns).eqls(['bytes_transferred', 'personal.name']); + expect(savedObjectAttributes.title).eq(saveName); + if (selectFields) { + expect(savedObjectAttributes.columns).eqls(SELECTED_FIELD_COLUMNS); } - if (checkSort) { - expect(savedObjectAttributes.sort).eqls([['bytes_transferred', 'desc']]); + if (sort) { + expect(savedObjectAttributes.sort).eqls(APPLIED_SORT); } expect(searchSource).match( // all special characters must be escaped new RegExp(`"query":"${queryString.replaceAll(/([*{}])/g, (char) => `\\${char}`)}"`) ); - expect(searchSource).match( - new RegExp(`"language":"${mapLanguageToApiResponseString(language)}"`) - ); + expect(searchSource).match(new RegExp(`"language":"${apiLanguage}"`)); expect(searchSource).match(new RegExp(`"title":"${dataset.replace('*', '\\*')}"`)); - expect(searchSource).match(new RegExp(`"type":"${mapDatasetToType(dataset)}"`)); + expect(searchSource).match(new RegExp(`"type":"${datasetType}"`)); - if (checkHistogramInterval) { + if (histogram) { expect(searchSource).match(/"calendar_interval":"1w"/); } - if (checkFilter) { - expect(searchSource).match(/"match_phrase":\{"category":"Application"\}/); + if (filters) { + expect(searchSource).match( + new RegExp(`"match_phrase":\{"${APPLIED_FILTERS.field}":"${APPLIED_FILTERS.value}"\}`) + ); } }); }; -const loadSavedSearch = (searchName, selectDuplicate = false) => { - cy.getElementByTestId('discoverOpenButton').click(); - if (selectDuplicate) { - cy.getElementByTestId(`savedObjectTitle${searchName}`).last().click(); - } else { - cy.getElementByTestId(`savedObjectTitle${searchName}`).first().click(); - } - - cy.get('h1').contains(searchName).should('be.visible'); -}; - -const setSearchAndSaveAndVerify = (config) => { - navigateToDiscoverPage(); - setDataset(config.dataset); - cy.setQueryLanguage(config.language); - setDatePickerDatesAndSearchIfRelevant(config.language); - - setSearchConfigurations({ - addFilter: config.filters, - queryString: prepareQueryStringForEditor(config.queryString, config.language, config.dataset), - setHistogramInterval: config.histogram, - selectFields: config.selectFields, - applySort: config.sort, - }); - verifyDiscoverPageState({ - dataset: config.dataset, - queryString: config.queryString, - language: config.language, - hitCount: config.hitCount, - checkFilter: config.filters, - checkHistogramInterval: config.histogram, - checkSelectedField: config.selectFields, - verifyTableData: config.sampleTableData, - }); - saveSearch({ searchName: config.saveName }); - - // There is a small chance where if we go to assets page, - // the saved search does not appear. So adding this wait - cy.wait(1000); - - verifySavedSearchInAssetsPage({ - dataset: config.dataset, - searchName: config.saveName, - queryString: config.queryString, - language: config.language, - checkHistogramInterval: config.histogram, - checkSelectedField: config.selectFields, - checkSort: config.sort, - checkFilter: config.filters, - }); -}; - -const loadSavedSearchAndVerify = (config) => { - // We are starting from various languages - // to guard against: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9078 - ['DQL', 'Lucene', 'OpenSearch SQL', 'PPL'].forEach((startingLanguage) => { - // TODO: Remove this line once bugs are fixed - // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9078 - if (startingLanguage !== config.language) return; - - navigateToDiscoverPage(); - cy.getElementByTestId('discoverNewButton').click(); - - // Intentionally setting INDEX_PATTERN dataset here so that - // we have access to all four languages that INDEX_PATTERN allows. - // This means that we are only testing loading a saved search - // starting from an INDEX_PATTERN dataset, but I think testing where the - // start is a permutation of other dataset is overkill - setDataset(INDEX_PATTERN_WITH_TIME); - - cy.setQueryLanguage(startingLanguage); - loadSavedSearch(config.saveName); - setDatePickerDatesAndSearchIfRelevant(config.language); - verifyDiscoverPageState({ - dataset: config.dataset, - queryString: config.queryString, - language: config.language, - hitCount: config.hitCount, - checkFilter: config.filters, - checkHistogramInterval: config.histogram, - checkSelectedField: config.selectFields, - verifyTableData: config.sampleTableData, - }); - }); -}; - -export const runSavedSearchCreateTests = () => { +export const runSavedSearchTests = () => { describe('saved search', () => { beforeEach(() => { // Load test data @@ -479,6 +382,15 @@ export const runSavedSearchCreateTests = () => { url: SECONDARY_ENGINE.url, authType: 'no_auth', }); + // Grab the data source ID + cy.contains('a', datasourceName).click(); + cy.url().then((url) => { + const urlWithoutSearchParams = url.split('?')[0]; + // split the URL into parts and filter out the empty ones + const urlParts = urlWithoutSearchParams.split('/').filter((parts) => !!parts.length); + datasourceId = urlParts[urlParts.length - 1]; + }); + // Create workspace cy.deleteWorkspaceByName(workspaceName); cy.visit('/app/home'); @@ -487,8 +399,19 @@ export const runSavedSearchCreateTests = () => { workspaceName: workspaceName, indexPattern: INDEX_PATTERN_WITH_TIME.replace('*', ''), timefieldName: 'timestamp', + dataSource: datasourceName, isEnhancement: true, }); + cy.url().then((url) => { + const urlWithoutSearchParams = url.split('?')[0]; + // split the URL into parts and filter out the empty ones + const urlParts = urlWithoutSearchParams.split('/').filter((parts) => !!parts.length); + // the index pattern path has a # at the end, so stripping it + indexPatternId = urlParts[urlParts.length - 1].replace('#', ''); + + const workspaceIdIndex = urlParts.findIndex((part) => part === 'w') + 1; + workspaceId = urlParts[workspaceIdIndex]; + }); }); afterEach(() => { @@ -499,15 +422,74 @@ export const runSavedSearchCreateTests = () => { cy.deleteIndex(INDEX_WITH_TIME_2); }); - allTestConfigurations.forEach((config) => { - it(`should successfully create a saved search and load it for ${mapDatasetToType( - config.dataset - ).toLowerCase()} ${config.language}`, () => { - setSearchAndSaveAndVerify(config); - loadSavedSearchAndVerify(config); + generateAllTestConfigurations().forEach((config) => { + it(`should successfully create a saved search for ${config.saveName}`, () => { + cy.navigateToWorkSpaceSpecificPage({ + workspaceName, + page: 'discover', + isEnhancement: true, + }); + + cy.setDataset(config.dataset, datasourceName, config.datasetType); + + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + + setSearchConfigurations(config); + verifyDiscoverPageState(config); + cy.saveSearch(config.saveName); + + // There is a small chance where if we go to assets page, + // the saved search does not appear. So adding this wait + cy.wait(2000); + + verifySavedSearchInAssetsPage(config); }); + + // We are starting from various languages + // to guard against: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9078 + Object.values(QueryLanguages) + .map((queryLanguage) => queryLanguage.name) + .forEach((startingLanguage) => { + // TODO: Remove this line once bugs are fixed + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/9078 + if (startingLanguage !== config.language) return; + + it(`should successfully load a saved search for ${config.saveName} starting from ${startingLanguage}`, () => { + // POST a saved search + cy.request({ + method: 'POST', + url: `/w/${workspaceId}/api/saved_objects/search?overwrite=true`, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'osd-xsrf': true, + }, + body: getSavedObjectPostBody(config), + failOnStatusCode: false, + }); + + cy.navigateToWorkSpaceSpecificPage({ + workspaceName, + page: 'discover', + isEnhancement: true, + }); + cy.getElementByTestId('discoverNewButton').click(); + + // Intentionally setting INDEX_PATTERN dataset here so that + // we have access to all four languages that INDEX_PATTERN allows. + // This means that we are only testing loading a saved search + // starting from an INDEX_PATTERN dataset, but I think testing where the + // start is a permutation of other dataset is overkill + cy.setIndexPatternAsDataset(INDEX_PATTERN_WITH_TIME, datasourceName); + + cy.setQueryLanguage(startingLanguage); + cy.loadSaveSearch(config.saveName, false); + setDatePickerDatesAndSearchIfRelevant(config.language); + verifyDiscoverPageState(config); + }); + }); }); }); }; -runSavedSearchCreateTests(); +runSavedSearchTests(); diff --git a/cypress/utils/apps/data_explorer/commands.js b/cypress/utils/apps/data_explorer/commands.js index 1e73e91584e..4e739569203 100644 --- a/cypress/utils/apps/data_explorer/commands.js +++ b/cypress/utils/apps/data_explorer/commands.js @@ -17,32 +17,53 @@ Cypress.Commands.add('verifyTimeConfig', (start, end) => { .should('have.text', end); }); -Cypress.Commands.add('saveSearch', (name) => { +Cypress.Commands.add('saveSearch', (name, saveAsNew = false) => { cy.log('in func save search'); const opts = { log: false }; cy.getElementByTestId('discoverSaveButton', opts).click(); cy.getElementByTestId('savedObjectTitle').clear().type(name); + + if (saveAsNew) { + cy.getElementByTestId('saveAsNewCheckbox').click(); + } + cy.getElementByTestId('confirmSaveSavedObjectButton').click({ force: true }); + // if saving as new save search, you need to click confirm twice; + if (saveAsNew) { + cy.getElementByTestId('confirmSaveSavedObjectButton').click(); + } + // Wait for page to load - cy.waitForLoader(); + cy.getElementByTestId('euiToastHeader').contains(/was saved/); }); -Cypress.Commands.add('loadSaveSearch', (name) => { +Cypress.Commands.add('loadSaveSearch', (name, selectDuplicate = false) => { const opts = { log: false, force: true, }; cy.getElementByTestId('discoverOpenButton', opts).click(opts); - cy.getElementByTestId(`savedObjectTitle${toTestId(name)}`).click(); + if (selectDuplicate) { + cy.getElementByTestId(`savedObjectTitle${toTestId(name)}`) + .last() + .click(); + } else { + cy.getElementByTestId(`savedObjectTitle${toTestId(name)}`) + .first() + .click(); + } - cy.waitForLoader(); + cy.get('h1').contains(name).should('be.visible'); }); Cypress.Commands.add('verifyHitCount', (count) => { - cy.getElementByTestId('discoverQueryHits').should('be.visible').should('have.text', count); + cy.getElementByTestId('discoverQueryHits') + .scrollIntoView() + .should('be.visible') + .should('have.text', count); }); Cypress.Commands.add('waitForSearch', () => { @@ -69,31 +90,40 @@ Cypress.Commands.add('verifyMarkCount', (count) => { cy.getElementByTestId('docTable').find('mark').should('have.length', count); }); -Cypress.Commands.add('submitFilterFromDropDown', (field, operator, value) => { - cy.getElementByTestId('addFilter').click(); - cy.getElementByTestId('filterFieldSuggestionList') - .should('be.visible') - .click() - .type(`${field}{downArrow}{enter}`) - .trigger('blur', { force: true }); - - cy.getElementByTestId('filterOperatorList') - .should('be.visible') - .click() - .type(`${operator}{downArrow}{enter}`) - .trigger('blur', { force: true }); +Cypress.Commands.add( + 'submitFilterFromDropDown', + (field, operator, value, isEnhancement = false) => { + if (isEnhancement) { + cy.getElementByTestId('showFilterActions').click(); + cy.getElementByTestId('addFilters').click(); + } else { + cy.getElementByTestId('addFilter').click(); + } + + cy.getElementByTestId('filterFieldSuggestionList') + .should('be.visible') + .click() + .type(`${field}{downArrow}{enter}`) + .trigger('blur', { force: true }); - if (value) { - cy.get('[data-test-subj^="filterParamsComboBox"]') + cy.getElementByTestId('filterOperatorList') .should('be.visible') .click() - .type(`${value}{downArrow}{enter}`) + .type(`${operator}{downArrow}{enter}`) .trigger('blur', { force: true }); - } - cy.getElementByTestId('saveFilter').click({ force: true }); - cy.waitForLoader(); -}); + if (value) { + cy.get('[data-test-subj^="filterParamsComboBox"]') + .should('be.visible') + .click() + .type(`${value}{downArrow}{enter}`) + .trigger('blur', { force: true }); + } + + cy.getElementByTestId('saveFilter').click({ force: true }); + cy.waitForLoader(isEnhancement); + } +); Cypress.Commands.add('saveQuery', (name, description) => { cy.whenTestIdNotFound('saved-query-management-popover', () => { diff --git a/cypress/utils/apps/data_explorer/index.d.ts b/cypress/utils/apps/data_explorer/index.d.ts index c3efc723ab1..ee101b5eb5f 100644 --- a/cypress/utils/apps/data_explorer/index.d.ts +++ b/cypress/utils/apps/data_explorer/index.d.ts @@ -6,14 +6,19 @@ declare namespace Cypress { interface Chainable { getTimeConfig(start: string, end: string): Chainable; - saveSearch(name: string): Chainable; - loadSaveSearch(name: string): Chainable; + saveSearch(name: string, saveAsNew?: boolean): Chainable; + loadSaveSearch(name: string, selectDuplicate?: boolean): Chainable; verifyHitCount(count: string): Chainable; waitForSearch(): Chainable; prepareTest(fromTime: string, toTime: string, interval: string): Chainable; submitQuery(query: string): Chainable; verifyMarkCount(count: string): Chainable; - submitFilterFromDropDown(field: string, operator: string, value: string): Chainable; + submitFilterFromDropDown( + field: string, + operator: string, + value: string, + isEnhancement?: boolean + ): Chainable; saveQuery(name: string, description: string): Chainable; loadSaveQuery(name: string): Chainable; clearSaveQuery(): Chainable; diff --git a/cypress/utils/apps/index.d.ts b/cypress/utils/apps/index.d.ts index 56ec83e71f0..2f95ef1b2be 100644 --- a/cypress/utils/apps/index.d.ts +++ b/cypress/utils/apps/index.d.ts @@ -32,6 +32,6 @@ declare namespace Cypress { * @example * cy.updateTopNav() */ - updateTopNav(): Chainable; + updateTopNav(opts: Record): Chainable; } } diff --git a/cypress/utils/apps/query_enhancements/commands.js b/cypress/utils/apps/query_enhancements/commands.js index 4b1714b4440..d68901928b0 100644 --- a/cypress/utils/apps/query_enhancements/commands.js +++ b/cypress/utils/apps/query_enhancements/commands.js @@ -3,9 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -Cypress.Commands.add('setQueryEditor', (value, submit = true) => { - const opts = { log: false }; - +Cypress.Commands.add('setQueryEditor', (value, opts = {}, submit = true) => { Cypress.log({ name: 'setQueryEditor', displayName: 'set query', @@ -27,7 +25,7 @@ Cypress.Commands.add('setQueryEditor', (value, submit = true) => { .type(value, opts); if (submit) { - cy.updateTopNav(opts); + cy.updateTopNav({ log: false }); } }); @@ -99,10 +97,57 @@ Cypress.Commands.add('deleteDataSourceByName', (dataSourceName) => { // Navigate to the dataSource Management page cy.visit('app/dataSources'); - // Find the anchor text correpsonding to specified dataSource + // Find the anchor text corresponding to specified dataSource cy.get('a').contains(dataSourceName).click(); // Delete the dataSource connection cy.getElementByTestId('editDatasourceDeleteIcon').click(); cy.getElementByTestId('confirmModalConfirmButton').click(); }); + +Cypress.Commands.add('setIndexAsDataset', (index, dataSourceName, language) => { + cy.getElementByTestId('datasetSelectorButton').should('be.visible').click(); + cy.getElementByTestId(`datasetSelectorAdvancedButton`).click(); + cy.get(`[title="Indexes"]`).click(); + cy.get(`[title="${dataSourceName}"]`).click(); + // this element is sometimes dataSourceName masked by another element + cy.get(`[title="${index}"]`).should('be.visible').click({ force: true }); + cy.getElementByTestId('datasetSelectorNext').click(); + + if (language) { + cy.getElementByTestId('advancedSelectorLanguageSelect').select(language); + } + + cy.getElementByTestId('advancedSelectorTimeFieldSelect').select('timestamp'); + cy.getElementByTestId('advancedSelectorConfirmButton').click(); + + // verify that it has been selected + cy.getElementByTestId('datasetSelectorButton').should( + 'contain.text', + `${dataSourceName}::${index}` + ); +}); + +Cypress.Commands.add('setIndexPatternAsDataset', (indexPattern, dataSourceName) => { + cy.getElementByTestId('datasetSelectorButton').should('be.visible').click(); + cy.get(`[title="${dataSourceName}::${indexPattern}"]`).click(); + + // verify that it has been selected + cy.getElementByTestId('datasetSelectorButton').should( + 'contain.text', + `${dataSourceName}::${indexPattern}` + ); +}); + +Cypress.Commands.add('setDataset', (dataset, dataSourceName, type) => { + switch (type) { + case 'INDEX_PATTERN': + cy.setIndexPatternAsDataset(dataset, dataSourceName); + break; + case 'INDEXES': + cy.setIndexAsDataset(dataset, dataSourceName); + break; + default: + throw new Error(`setIndexPatternAsDataset encountered unknown type: ${type}`); + } +}); diff --git a/cypress/utils/apps/query_enhancements/index.d.ts b/cypress/utils/apps/query_enhancements/index.d.ts index 5955e524417..002e15edbc6 100644 --- a/cypress/utils/apps/query_enhancements/index.d.ts +++ b/cypress/utils/apps/query_enhancements/index.d.ts @@ -5,7 +5,11 @@ declare namespace Cypress { interface Chainable { - setQueryEditor(value: string, submit?: boolean): Chainable; + setQueryEditor( + value: string, + opts?: { parseSpecialCharSequences?: boolean }, + submit?: boolean + ): Chainable; setQueryLanguage(value: 'DQL' | 'Lucene' | 'OpenSearch SQL' | 'PPL'): Chainable; addDataSource(opts: { name: string; @@ -14,5 +18,16 @@ declare namespace Cypress { credentials?: { username: string; password: string }; }): Chainable; deleteDataSourceByName(dataSourceName: string): Chainable; + setIndexAsDataset( + index: string, + dataSourceName: string, + language?: 'OpenSearch SQL' | 'PPL' + ): Chainable; + setIndexPatternAsDataset(indexPattern: string, dataSourceName: string): Chainable; + setDataset( + dataset: string, + dataSourceName: string, + type: 'INDEXES' | 'INDEX_PATTERN' + ): Chainable; } } diff --git a/cypress/utils/dashboards/commands.js b/cypress/utils/dashboards/commands.js index d26fd09aeaf..7a4b2f45495 100644 --- a/cypress/utils/dashboards/commands.js +++ b/cypress/utils/dashboards/commands.js @@ -45,7 +45,6 @@ Cypress.Commands.add( cy.waitForLoader(); } ); - Cypress.Commands.add( // navigates to the workspace HomePage of a given workspace 'navigateToWorkSpaceHomePage', @@ -53,6 +52,8 @@ Cypress.Commands.add( // Selecting the correct workspace cy.visit('/app/workspace_list#'); cy.openWorkspaceDashboard(workspaceName); + // wait until page loads + cy.getElementByTestId('headerAppActionMenu').should('be.visible'); } ); @@ -63,7 +64,6 @@ Cypress.Commands.add( const { workspaceName, page, isEnhancement = false } = opts; // Navigating to the WorkSpace Home Page cy.navigateToWorkSpaceHomePage(workspaceName); - cy.waitForLoader(isEnhancement); // Check for toggleNavButton and handle accordingly // If collapsibleNavShrinkButton is shown which means toggleNavButton is already clicked, try clicking the app link directly