diff --git a/changelogs/fragments/9229.yml b/changelogs/fragments/9229.yml new file mode 100644 index 000000000000..970934ff73f9 --- /dev/null +++ b/changelogs/fragments/9229.yml @@ -0,0 +1,2 @@ +test: +- Add cypress integration test for the old and new UI view saved queries. ([#9229](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9229)) \ No newline at end of file diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries.spec.js new file mode 100644 index 000000000000..c3d6e62c0d94 --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries.spec.js @@ -0,0 +1,164 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_PATTERN_WITH_TIME, + INDEX_WITH_TIME_1, + INDEX_WITH_TIME_2, + SECONDARY_ENGINE, +} from '../../../../../utils/constants'; + +import { + verifyDiscoverPageState, + verifyQueryDoesNotExistInSavedQueries, + setQueryConfigurations, + updateAndVerifySavedQuery, + SAVE_AS_NEW_QUERY_SUFFIX, + validateSaveAsNewQueryMatchingNameHasError, +} from '../../../../../utils/apps/query_enhancements/saved_queries'; + +import { + getRandomizedWorkspaceName, + getRandomizedDatasourceName, + setDatePickerDatesAndSearchIfRelevant, + generateAllTestConfigurations, +} from '../../../../../utils/apps/query_enhancements/shared'; + +import { generateSavedTestConfiguration } from '../../../../../utils/apps/query_enhancements/saved'; + +const workspaceName = getRandomizedWorkspaceName(); +const datasourceName = getRandomizedDatasourceName(); + +const createSavedQuery = (config) => { + cy.navigateToWorkSpaceSpecificPage({ + workspaceName, + page: 'discover', + isEnhancement: true, + }); + + cy.setDataset(config.dataset, datasourceName, config.datasetType); + + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + + setQueryConfigurations(config); + verifyDiscoverPageState(config); + + cy.saveQuery(config.saveName, ' ', true, true); +}; + +const loadSavedQuery = (config) => { + cy.navigateToWorkSpaceSpecificPage({ + workspaceName, + page: 'discover', + isEnhancement: true, + }); + + cy.getElementByTestId('discoverNewButton').click(); + cy.setDataset(config.dataset, datasourceName, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant( + config.language, + 'Aug 29, 2020 @ 00:00:00.000', + 'Aug 30, 2020 @ 00:00:00.000' + ); + + cy.loadSaveQuery(config.saveName); + // wait for saved queries to load. + cy.getElementByTestId('docTable').should('be.visible'); + verifyDiscoverPageState(config); +}; + +const modifyAndVerifySavedQuery = (config, saveAsNewQueryName) => { + if (config.filters) { + cy.deleteAllFilters(); + } + setDatePickerDatesAndSearchIfRelevant(config.language); + + setQueryConfigurations(config); + verifyDiscoverPageState(config); + validateSaveAsNewQueryMatchingNameHasError(config.saveName); + cy.updateSaveQuery(saveAsNewQueryName, true, true, true); + + cy.reload(); + cy.loadSaveQuery(saveAsNewQueryName); + // wait for saved query to load + cy.getElementByTestId('docTable').should('be.visible'); + verifyDiscoverPageState(config); +}; + +const deleteSavedQuery = (saveAsNewQueryName) => { + cy.navigateToWorkSpaceSpecificPage({ + workspaceName, + page: 'discover', + isEnhancement: true, + }); + + cy.deleteSaveQuery(saveAsNewQueryName); + verifyQueryDoesNotExistInSavedQueries(saveAsNewQueryName); +}; + +// This spec assumes data.savedQueriesNewUI.enabled is true. +export const runSavedQueriesUITests = () => { + describe('saved queries UI', () => { + 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.osd.createInitialWorkspaceWithDataSource(datasourceName, workspaceName); + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: INDEX_PATTERN_WITH_TIME.replace('*', ''), + timefieldName: 'timestamp', + dataSource: datasourceName, + isEnhancement: true, + }); + }); + + afterEach(() => { + // No need to explicitly delete all saved queries as deleting the workspace will delete associated saved queries + 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); + }); + + const testConfigurations = generateAllTestConfigurations(generateSavedTestConfiguration); + + testConfigurations.forEach((config) => { + it(`should create, load, update, modify and delete the saved query: ${config.testName}`, () => { + createSavedQuery(config); + loadSavedQuery(config); + updateAndVerifySavedQuery(config); + + const saveAsNewQueryName = config.testName + SAVE_AS_NEW_QUERY_SUFFIX; + modifyAndVerifySavedQuery(config, saveAsNewQueryName); + deleteSavedQuery(saveAsNewQueryName); + }); + }); + }); +}; + +runSavedQueriesUITests(); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries_flyout.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries_flyout.spec.js deleted file mode 100644 index 2bd6e2d232b8..000000000000 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries_flyout.spec.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - INDEX_PATTERN_WITH_TIME, - INDEX_WITH_TIME_1, - INDEX_WITH_TIME_2, - SECONDARY_ENGINE, -} from '../../../../../utils/constants'; -import { - getRandomizedWorkspaceName, - getRandomizedDatasourceName, - setDatePickerDatesAndSearchIfRelevant, - generateAllTestConfigurations, -} from '../../../../../utils/apps/query_enhancements/shared'; -import { - generateSavedTestConfiguration, - setSearchConfigurations, - verifyDiscoverPageState, -} from '../../../../../utils/apps/query_enhancements/saved'; - -const workspaceName = getRandomizedWorkspaceName(); -const datasourceName = getRandomizedDatasourceName(); - -// This spec assumes data.savedQueriesNewUI.enabled is true. -export const runSavedQueriesFlyoutUITests = () => { - describe('saved queries flyout UI', () => { - 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.osd.createInitialWorkspaceWithDataSource(datasourceName, workspaceName); - cy.createWorkspaceIndexPatterns({ - workspaceName: workspaceName, - indexPattern: INDEX_PATTERN_WITH_TIME.replace('*', ''), - timefieldName: 'timestamp', - dataSource: datasourceName, - isEnhancement: true, - }); - }); - - afterEach(() => { - // No need to explicitly delete all saved queries as deleting the workspace will delete associated saved queries - 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); - }); - - generateAllTestConfigurations(generateSavedTestConfiguration).forEach((config) => { - it(`should successfully create a saved query for ${config.testName}`, () => { - 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.saveQuery(config.saveName); - - // verify that it has been saved - cy.getElementByTestId('saved-query-management-popover-button').click(); - cy.getElementByTestId('saved-query-management-open-button').click(); - cy.getElementByTestId('euiFlyoutCloseButton') - .parent() - .contains(config.saveName) - .should('exist'); - }); - }); - }); -}; - -runSavedQueriesFlyoutUITests(); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries_popover.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries_popover.spec.js deleted file mode 100644 index 92162b26fe94..000000000000 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/saved_queries_popover.spec.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - INDEX_PATTERN_WITH_TIME, - INDEX_WITH_TIME_1, - INDEX_WITH_TIME_2, - SECONDARY_ENGINE, -} from '../../../../../utils/constants'; -import { - getRandomizedWorkspaceName, - getRandomizedDatasourceName, - setDatePickerDatesAndSearchIfRelevant, - generateAllTestConfigurations, -} from '../../../../../utils/apps/query_enhancements/shared'; -import { - generateSavedTestConfiguration, - setSearchConfigurations, - verifyDiscoverPageState, -} from '../../../../../utils/apps/query_enhancements/saved'; - -const workspaceName = getRandomizedWorkspaceName(); -const datasourceName = getRandomizedDatasourceName(); - -// This spec assumes data.savedQueriesNewUI.enabled is false. -// These tests will not be run until the older legacy tests are migrated https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9166#discussion_r1913687440 -export const runSavedQueriesPopoverUITests = () => { - describe.skip('saved queries popover UI', () => { - 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.osd.createInitialWorkspaceWithDataSource(datasourceName, workspaceName); - cy.createWorkspaceIndexPatterns({ - workspaceName: workspaceName, - indexPattern: INDEX_PATTERN_WITH_TIME.replace('*', ''), - timefieldName: 'timestamp', - dataSource: datasourceName, - isEnhancement: true, - }); - }); - - afterEach(() => { - // No need to explicitly delete all saved queries as deleting the workspace will delete associated saved queries - 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); - }); - - generateAllTestConfigurations(generateSavedTestConfiguration).forEach((config) => { - it(`should successfully create a saved query for ${config.testName}`, () => { - 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.saveQuery(config.saveName); - - // verify that it has been saved - cy.getElementByTestId('saved-query-management-popover-button').click(); - cy.getElementByTestId('saved-query-management-popover') - .contains(config.saveName) - .should('exist'); - }); - }); - }); -}; - -runSavedQueriesPopoverUITests(); diff --git a/cypress/utils/apps/data_explorer/commands.js b/cypress/utils/apps/data_explorer/commands.js index d8c1383d3d16..54f30c682e62 100644 --- a/cypress/utils/apps/data_explorer/commands.js +++ b/cypress/utils/apps/data_explorer/commands.js @@ -113,26 +113,82 @@ Cypress.Commands.add( } ); -Cypress.Commands.add('saveQuery', (name, description = ' ') => { - cy.whenTestIdNotFound('saved-query-management-popover', () => { - cy.getElementByTestId('saved-query-management-popover-button').click(); - }); - cy.getElementByTestId('saved-query-management-save-button').click(); +Cypress.Commands.add('deleteAllFilters', () => { + // the Close icon is from elastic. + cy.getElementsByTestIds('globalFilterBar').find('[class="euiBadge__iconButton"]').click(); +}); - cy.getElementByTestId('saveQueryFormTitle').type(name); - cy.getElementByTestId('saveQueryFormDescription').type(description); +Cypress.Commands.add( + 'saveQuery', + (name, description = ' ', includeFilters = true, includeTimeFilter = false) => { + cy.whenTestIdNotFound('saved-query-management-popover', () => { + cy.getElementByTestId('saved-query-management-popover-button').click(); + }); + cy.getElementByTestId('saved-query-management-save-button').click(); - // putting force: true as this button is sometimes masked by a popup element - cy.getElementByTestId('savedQueryFormSaveButton').click({ force: true }); - cy.getElementByTestId('euiToastHeader').contains('was saved').should('be.visible'); -}); + cy.getElementByTestId('saveQueryFormTitle').type(name); + cy.getElementByTestId('saveQueryFormDescription').type(description); + + if (includeFilters !== true) { + cy.getElementByTestId('saveQueryFormIncludeFiltersOption').click(); + } + + if (includeTimeFilter !== false) { + cy.getElementByTestId('saveQueryFormIncludeTimeFilterOption').click(); + } + + // The force is necessary as there is occasionally a popover that covers the button + cy.getElementByTestId('savedQueryFormSaveButton').click({ force: true }); + cy.getElementByTestId('euiToastHeader').contains('was saved').should('be.visible'); + } +); + +Cypress.Commands.add( + 'updateSaveQuery', + (name = '', saveAsNewQuery = false, includeFilters = true, includeTimeFilter = false) => { + cy.whenTestIdNotFound('saved-query-management-popover', () => { + cy.getElementByTestId('saved-query-management-popover-button').click(); + }); + cy.getElementByTestId('saved-query-management-save-button').click(); + + if (saveAsNewQuery) { + cy.getElementByTestId('saveAsNewQueryCheckbox') + .parent() + .find('[class="euiCheckbox__label"]') + .click(); + cy.getElementByTestId('saveQueryFormTitle').should('not.be.disabled').type(name); + + // Selecting the saveAsNewQuery element deselects the include time filter option. + if (includeTimeFilter === true) { + cy.getElementByTestId('saveQueryFormIncludeTimeFilterOption').click(); + } + } else if (saveAsNewQuery === false) { + // defaults to not selected. + + if (includeTimeFilter !== true) { + cy.getElementByTestId('saveQueryFormIncludeTimeFilterOption').click(); + } + } + + if (includeFilters !== true) { + // Always defaults to selected. + cy.getElementByTestId('saveQueryFormIncludeFiltersOption').click(); + } + + // The force is necessary as there is occasionally a popover that covers the button + cy.getElementByTestId('savedQueryFormSaveButton').click({ force: true }); + cy.getElementByTestId('euiToastHeader').contains('was saved').should('be.visible'); + } +); Cypress.Commands.add('loadSaveQuery', (name) => { - cy.getElementByTestId('saved-query-management-popover-button').click({ - force: true, - }); + cy.getElementByTestId('saved-query-management-popover-button').click(); - cy.get(`[data-test-subj~="load-saved-query-${name}-button"]`).should('be.visible').click(); + cy.getElementByTestId('saved-query-management-open-button').click(); + + cy.getElementByTestId('euiFlyoutCloseButton').parent().contains(name).should('exist').click(); + // click button through popover + cy.getElementByTestId('open-query-action-button').click({ force: true }); }); Cypress.Commands.add('clearSaveQuery', () => { @@ -146,9 +202,13 @@ Cypress.Commands.add('clearSaveQuery', () => { Cypress.Commands.add('deleteSaveQuery', (name) => { cy.getElementByTestId('saved-query-management-popover-button').click(); - cy.get(`[data-test-subj~="delete-saved-query-${name}-button"]`).click({ - force: true, - }); + cy.getElementByTestId('saved-query-management-open-button').click(); + cy.getElementByTestId('euiFlyoutCloseButton') + .parent() + .contains(name) + .findElementByTestId('deleteSavedQueryButton') + .click(); + cy.getElementByTestId('confirmModalConfirmButton').click(); }); diff --git a/cypress/utils/apps/data_explorer/index.d.ts b/cypress/utils/apps/data_explorer/index.d.ts index 12912ecbffb2..95805f36e5a5 100644 --- a/cypress/utils/apps/data_explorer/index.d.ts +++ b/cypress/utils/apps/data_explorer/index.d.ts @@ -19,7 +19,40 @@ declare namespace Cypress { value: string, isEnhancement?: boolean ): Chainable; - saveQuery(name: string, description?: string): Chainable; + /** + * Deletes all filters in a Search. + */ + deleteAllFilters(): void; + /** + * Save a query. + * @param name - Name of the query + * @param description - Description of the query + * @param includeFilters - Save filters + * @param includeTimeFilter - Save Time filter + */ + saveQuery( + name: string, + description?: string, + includeFilters?: boolean, + includeTimeFilter?: boolean + ): Chainable; + /** + * Update a saved query. + * @param name - Name of saved query if saved as new query. + * @param saveAsNewQuery - Save as a new query. + * @param includeFilters - Save filters. + * @param includeTimeFilter - Save Time filter. + */ + updateSaveQuery( + name: string, + saveAsNewQuery: boolean, + includeFilters?: boolean, + includeTimeFilter?: boolean + ): void; + /** + * Load a saved query. + * @param name - Name of saved query. + */ loadSaveQuery(name: string): Chainable; clearSaveQuery(): Chainable; deleteSaveQuery(name: string): Chainable; diff --git a/cypress/utils/apps/query_enhancements/saved.js b/cypress/utils/apps/query_enhancements/saved.js index d6bce6482240..7fd4d9bcd570 100644 --- a/cypress/utils/apps/query_enhancements/saved.js +++ b/cypress/utils/apps/query_enhancements/saved.js @@ -10,6 +10,8 @@ import { INDEX_PATTERN_WITH_TIME, INDEX_WITH_TIME_1, QueryLanguages, + START_TIME, + END_TIME, } from './constants'; import { setDatePickerDatesAndSearchIfRelevant } from './shared'; @@ -55,7 +57,7 @@ export const APPLIED_FILTERS = { * @param {QueryEnhancementLanguage} language - the name of query language * @returns {string} */ -const getQueryString = (dataset, language) => { +export const getQueryString = (dataset, language) => { switch (language) { case QueryLanguages.DQL.name: return 'bytes_transferred > 9950'; @@ -76,7 +78,7 @@ const getQueryString = (dataset, language) => { * @param {QueryEnhancementLanguage} language - the query language name * @returns {number|undefined} */ -const getExpectedHitCount = (datasetType, language) => { +export const getExpectedHitCount = (datasetType, language) => { switch (datasetType) { case DatasetTypes.INDEX_PATTERN.name: switch (language) { @@ -117,7 +119,7 @@ const getExpectedHitCount = (datasetType, language) => { * @param {QueryEnhancementLanguage} language - the query language name * @returns {[[number,string]]|*[]} An array of table data. For each element, 0th index is the index of the table cell, and 1st index is the value in that table cell */ -const getSampleTableData = (datasetType, language) => { +export const getSampleTableData = (datasetType, language) => { switch (datasetType) { case DatasetTypes.INDEX_PATTERN.name: switch (language) { @@ -165,6 +167,8 @@ const getSampleTableData = (datasetType, language) => { * @property {string} apiLanguage - the name of query language as recognized by OpenSearch API * @property {string} saveName - the name to use when saving the saved search * @property {string} testName - the phrase to add to the test case's title + * @property {string} startTime - the absolute start time for the query in the form e.g. Jan 1, 2020 @ 15:17:18.005 + * @property {string} endTime - the absolute end time for the query in the form e.g. Jan 1, 2020 @ 15:17:18.005 * @property {boolean} filters - whether the language supports filtering * @property {boolean} histogram - whether the language supports histogram * @property {boolean} selectFields - whether the language supports selecting fields to view data @@ -189,6 +193,8 @@ export const generateSavedTestConfiguration = (dataset, datasetType, language) = apiLanguage: language.apiName, saveName: `${language.name}-${datasetType}`, testName: `${language.name}-${datasetType}`, + startTime: START_TIME, + endTime: END_TIME, ...language.supports, }; diff --git a/cypress/utils/apps/query_enhancements/saved_queries.js b/cypress/utils/apps/query_enhancements/saved_queries.js new file mode 100644 index 000000000000..7da35d62c604 --- /dev/null +++ b/cypress/utils/apps/query_enhancements/saved_queries.js @@ -0,0 +1,334 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatasetTypes, QueryLanguages } from './constants'; + +import { APPLIED_FILTERS } from './saved'; + +import { setDatePickerDatesAndSearchIfRelevant } from './shared'; + +/** + * Error text when there is a name conflict when saving a query. + * @constant + * @type {string} + * @default + */ +const SAVE_QUERY_CONFLICT_NAME_ERROR_TEXT = 'Name conflicts with an existing saved query'; + +/** + * Suffix when saving as new query. + * @constant + * @type {string} + * @default + */ +export const SAVE_AS_NEW_QUERY_SUFFIX = '-1'; + +/** + * The Alternate Absolute Start Time to use for saved query + * @constant + * @type {string} + * @default + */ +const ALTERNATE_START_TIME = 'Mar 21, 2021 @ 15:23:04.251'; + +/** + * The Alternate Absolute End Time to use for saved query + * @constant + * @type {string} + * @default + */ +const ALTERNATE_END_TIME = 'Aug 28, 2021 @ 15:38:14.646'; + +/** + * The alternate filter configuration to use for saved query + * @constant + * @type {string} + * @default + */ +export const ALTERNATE_APPLIED_FILTERS = { + field: 'category', + operator: 'is not one of', + value: 'Database', +}; + +/** + * Returns the query string to use for a given dataset+language that is different from the output of getQueryString function + * @param {string} dataset - the dataset name to use + * @param {QueryEnhancementLanguage} language - the name of query language + * @returns {string} + */ +const getAlternateQueryString = (dataset, language) => { + switch (language) { + case QueryLanguages.DQL.name: + return 'bytes_transferred < 200'; + case QueryLanguages.Lucene.name: + return 'bytes_transferred: {* TO 200}'; + case QueryLanguages.SQL.name: + return `SELECT * FROM ${dataset} WHERE bytes_transferred < 200`; + case QueryLanguages.PPL.name: + return `source = ${dataset} | where bytes_transferred < 200`; + default: + throw new Error(`getQueryString encountered unsupported language: ${language}`); + } +}; + +/** + * Returns the expected hit count, if relevant, for the provided datasetType + language that is different from the output of getExpectedHitCount function + * @param {QueryEnhancementDataset} datasetType - the type of the dataset + * @param {QueryEnhancementLanguage} language - the query language name + * @returns {number|undefined} + */ +const getAlternateExpectedHitCount = (datasetType, language) => { + switch (datasetType) { + case DatasetTypes.INDEX_PATTERN.name: + switch (language) { + case QueryLanguages.DQL.name: + return 30; + case QueryLanguages.Lucene.name: + return 30; + case QueryLanguages.SQL.name: + return undefined; + case QueryLanguages.PPL.name: + // TODO: Update this to 45 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: + throw new Error(`getExpectedHitCount encountered unsupported datasetType: ${datasetType}`); + } +}; + +/** + * Returns an alternate SavedTestConfig + * @param {SavedTestConfig} config - initial config that will be modified. + * @returns {SavedTestConfig} + */ +const generateAlternateTestConfiguration = (config) => { + const baseConfig = { + dataset: config.dataset, + datasetType: config.datasetType, + language: config.language, + apiLanguage: config.apiLanguage, + saveName: config.saveName, + testName: config.testName, + startTime: ALTERNATE_START_TIME, + endTime: ALTERNATE_END_TIME, + filters: config.filters, + histogram: config.histogram, + selectFields: config.selectFields, + sort: config.sort, + }; + + return { + ...baseConfig, + queryString: getAlternateQueryString(config.dataset, config.language), + hitCount: getAlternateExpectedHitCount(config.datasetType, config.language), + }; +}; + +/** + * Verify that the discover page is in the correct state after loading a saved query have been run + * @param {SavedTestConfig} testConfig - the relevant config for the test case + */ +export const verifyDiscoverPageState = ({ + queryString, + language, + hitCount, + filters, + histogram, + startTime, + endTime, +}) => { + 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 (filters) { + cy.getElementByTestId( + `filter filter-enabled filter-key-${APPLIED_FILTERS.field} filter-value-${APPLIED_FILTERS.value} filter-unpinned ` + ).should('exist'); + } + if (hitCount) { + cy.verifyHitCount(hitCount); + } + + 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 (language !== QueryLanguages.SQL.name) { + cy.getElementByTestId('osdQueryEditorUpdateButton').contains(startTime).should('exist'); + cy.getElementByTestId('osdQueryEditorUpdateButton').contains(endTime).should('exist'); + } +}; + +/** + * Verify that the discover page is in the correct state after loading a saved query have been run + * @param {SavedTestConfig} testConfig - the relevant config for the test case + */ +export const verifyAlternateDiscoverPageState = ({ + queryString, + language, + hitCount, + filters, + histogram, + startTime, + endTime, +}) => { + 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 (filters) { + cy.getElementByTestId( + `filter filter-enabled filter-key-${ALTERNATE_APPLIED_FILTERS.field} filter-value-${ALTERNATE_APPLIED_FILTERS.value} filter-unpinned filter-negated` + ).should('exist'); + } + if (hitCount) { + cy.verifyHitCount(hitCount); + } + + 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 (language !== QueryLanguages.SQL.name) { + cy.getElementByTestId('osdQueryEditorUpdateButton').contains(startTime).should('exist'); + cy.getElementByTestId('osdQueryEditorUpdateButton').contains(endTime).should('exist'); + } +}; + +/** + * Set the query configurations for the saved query + * @param {SavedTestConfig} testConfig - the relevant config for the test case + */ +export const setQueryConfigurations = ({ filters, queryString, histogram }) => { + if (filters) { + cy.submitFilterFromDropDown( + APPLIED_FILTERS.field, + APPLIED_FILTERS.operator, + APPLIED_FILTERS.value, + true + ); + } + + cy.setQueryEditor(queryString, { parseSpecialCharSequences: false }); + + if (histogram) { + cy.getElementByTestId('discoverIntervalSelect').select('w'); + } +}; + +/** + * Set the query configurations for the saved query + * @param {SavedTestConfig} testConfig - the relevant config for the test case + */ +export const setAlternateQueryConfigurations = ({ filters, queryString, histogram }) => { + if (filters) { + cy.submitFilterFromDropDown( + ALTERNATE_APPLIED_FILTERS.field, + ALTERNATE_APPLIED_FILTERS.operator, + ALTERNATE_APPLIED_FILTERS.value, + true + ); + } + + cy.setQueryEditor(queryString, { parseSpecialCharSequences: false }); + + if (histogram) { + cy.getElementByTestId('discoverIntervalSelect').select('w'); + } +}; + +/** + * Verify query does not exist in saved queries. + * @param {string} deletedQueryName - Name of the query that should not exist. + */ +export const verifyQueryDoesNotExistInSavedQueries = (deletedQueryName) => { + cy.reload(); + cy.getElementByTestId('saved-query-management-popover-button').click(); + cy.getElementByTestId('saved-query-management-open-button').click(); + cy.getElementByTestId('savedQueriesFlyoutBody').contains(deletedQueryName).should('not.exist'); + // Two references to two buttons layered over each other. + cy.getElementByTestId('euiFlyoutCloseButton').first().click({ force: true }); +}; + +/** + * Update and save the saved query with alternate config, then verify has been updated correctly. + * @param {SavedTestConfig} config - the config for the test case to be updated + */ +export const updateAndVerifySavedQuery = (config) => { + // Create alternate config + const alternateConfig = generateAlternateTestConfiguration(config); + cy.loadSaveQuery(config.saveName); + + // wait for saved query to load + cy.getElementByTestId('docTable').should('be.visible'); + + if (alternateConfig.filters) { + cy.deleteAllFilters(); + } + + setDatePickerDatesAndSearchIfRelevant(config.language, ALTERNATE_START_TIME, ALTERNATE_END_TIME); + + setAlternateQueryConfigurations(alternateConfig); + verifyAlternateDiscoverPageState(alternateConfig); + cy.updateSaveQuery('', false, true, true); + + cy.reload(); + cy.loadSaveQuery(config.saveName); + // wait for saved query to load + cy.getElementByTestId('docTable').should('be.visible'); + verifyAlternateDiscoverPageState(alternateConfig); +}; + +/** + * Save as new query, and validate that saving as an existing name leads to the correct error message. + * @param {string} matchingName - the name of an existing saved query + */ +export const validateSaveAsNewQueryMatchingNameHasError = (matchingName) => { + cy.whenTestIdNotFound('saved-query-management-popover', () => { + cy.getElementByTestId('saved-query-management-popover-button').click(); + }); + cy.getElementByTestId('saved-query-management-save-button').click(); + + cy.getElementByTestId('saveAsNewQueryCheckbox') + .parent() + .find('[class="euiCheckbox__label"]') + .click(); + cy.getElementByTestId('saveQueryFormTitle').should('not.be.disabled').type(matchingName); + + // The force is necessary as there is occasionally a popover that covers the button + cy.getElementByTestId('savedQueryFormSaveButton').click({ force: true }); + + cy.contains(SAVE_QUERY_CONFLICT_NAME_ERROR_TEXT).should('be.visible'); + // Two references to two buttons layered over each other. + cy.getElementByTestId('euiFlyoutCloseButton').first().click({ force: true }); +}; diff --git a/cypress/utils/apps/query_enhancements/shared.js b/cypress/utils/apps/query_enhancements/shared.js index 9fe5a24512f4..e5d09bb4aa12 100644 --- a/cypress/utils/apps/query_enhancements/shared.js +++ b/cypress/utils/apps/query_enhancements/shared.js @@ -72,9 +72,15 @@ export const generateAllTestConfigurations = (generateTestConfigurationCallback) /** * Sets the top nav date if it is relevant for the passed language * @param {QueryEnhancementLanguage} language - query language + * @param {string=} start - start datetime string + * @param {string=} end - end datetime string */ -export const setDatePickerDatesAndSearchIfRelevant = (language) => { +export const setDatePickerDatesAndSearchIfRelevant = ( + language, + start = START_TIME, + end = END_TIME +) => { if (language !== QueryLanguages.SQL.name) { - cy.setTopNavDate(START_TIME, END_TIME); + cy.setTopNavDate(start, end); } }; diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index 01e072624666..e2287c292181 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -28,6 +28,10 @@ Cypress.Commands.add('getElementByTestId', (testId, options = {}) => { return cy.get(`[data-test-subj="${testId}"]`, options); }); +Cypress.Commands.add('getElementByTestIdLike', (testId, options = {}) => { + return cy.get(`[data-test-subj*="${testId}"]`, options); +}); + Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => { const selectors = [testIds].flat(Infinity).map((testId) => `[data-test-subj="${testId}"]`); return cy.get(selectors.join(','), options); diff --git a/cypress/utils/index.d.ts b/cypress/utils/index.d.ts index 8d8b29fa992d..f6696c8c16e2 100644 --- a/cypress/utils/index.d.ts +++ b/cypress/utils/index.d.ts @@ -24,6 +24,15 @@ declare namespace Cypress { testIds: string | string[], options?: Partial ): Chainable; + /** + * Get an element which contains testId + * @example + * cy.getElementByTestIdLike('query') + */ + getElementByTestIdLike( + testId: string, + options?: Partial + ): Chainable; /** * Get an element by its test id * @example