diff --git a/changelogs/fragments/9112.yml b/changelogs/fragments/9112.yml new file mode 100644 index 000000000000..dbeadb7b9289 --- /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 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 a132235fe2db..252a5dca71f6 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 @@ -9,7 +9,7 @@ import { START_TIME, END_TIME, } from '../../../../../utils/apps/constants'; -import { BASE_PATH, SECONDARY_ENGINE } from '../../../../../utils/constants'; +import { SECONDARY_ENGINE } from '../../../../../utils/constants'; const randomString = Math.random().toString(36).substring(7); const workspace = `${WORKSPACE_NAME}-${randomString}`; @@ -38,6 +38,11 @@ describe('dataset selector', { scrollBehavior: false }, () => { cy.deleteWorkspaceByName(`${workspace}`); cy.visit('/app/home'); cy.createInitialWorkspaceWithDataSource(`${DATASOURCE_NAME}`, `${workspace}`); + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspace, + page: 'discover', + isEnhancement: true, + }); }); afterEach(() => { @@ -46,21 +51,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'); @@ -78,22 +69,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'); @@ -119,8 +95,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}`, + workspaceName: workspace, indexPattern: 'data_logs_small_time_1', timefieldName: 'timestamp', indexPatternHasTimefield: true, @@ -128,13 +103,12 @@ describe('dataset selector', { scrollBehavior: false }, () => { isEnhancement: true, }); - cy.navigateToWorkSpaceHomePage(`${BASE_PATH}`, `${workspace}`); + cy.navigateToWorkSpaceHomePage(workspace); 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/queries.spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/queries.spec.js index 294f535bc088..500490acdd08 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 @@ -9,7 +9,7 @@ import { START_TIME, END_TIME, } from '../../../../../utils/apps/constants'; -import { BASE_PATH, SECONDARY_ENGINE } from '../../../../../utils/constants'; +import { SECONDARY_ENGINE } from '../../../../../utils/constants'; const randomString = Math.random().toString(36).substring(7); const workspace = `${WORKSPACE_NAME}-${randomString}`; @@ -37,8 +37,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}`, + workspaceName: workspace, indexPattern: 'data_logs_small_time_1', timefieldName: 'timestamp', indexPatternHasTimefield: true, @@ -46,13 +45,16 @@ describe('query enhancement queries', { scrollBehavior: false }, () => { isEnhancement: true, }); - // Go to workspace home - cy.navigateToWorkSpaceHomePage(`${BASE_PATH}`, `${workspace}`); - cy.waitForLoader(true); + // Go to discover page + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspace, + page: 'discover', + isEnhancement: true, + }); }); after(() => { - cy.deleteWorkspaceByName(`${workspace}`); + cy.deleteWorkspaceByName(workspace); cy.deleteDataSourceByName(`${DATASOURCE_NAME}`); cy.deleteIndex('data_logs_small_time_1'); }); @@ -63,7 +65,7 @@ describe('query enhancement queries', { scrollBehavior: false }, () => { cy.setTopNavDate(START_TIME, END_TIME); const query = `_id:1`; - cy.setSingleLineQueryEditor(query); + cy.setQueryEditor(query); cy.waitForLoader(true); cy.waitForSearch(); cy.verifyHitCount(1); @@ -78,7 +80,7 @@ describe('query enhancement queries', { scrollBehavior: false }, () => { cy.setTopNavDate(START_TIME, END_TIME); 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 000000000000..f4cbdb228850 --- /dev/null +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/saved_search.spec.js @@ -0,0 +1,127 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_PATTERN_WITH_TIME, + INDEX_WITH_TIME_1, + INDEX_WITH_TIME_2, + QueryLanguages, + SECONDARY_ENGINE, +} from '../../../../../utils/constants'; +import { + workspaceName, + datasourceName, + generateAllTestConfigurations, + setDatePickerDatesAndSearchIfRelevant, + setSearchConfigurations, + verifyDiscoverPageState, + verifySavedSearchInAssetsPage, + postRequestSaveSearch, +} from './utils/saved_search'; + +export const runSavedSearchTests = () => { + 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', + dataSource: datasourceName, + 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); + }); + + generateAllTestConfigurations().forEach((config) => { + it(`should successfully create a saved search 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.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.testName} starting from ${startingLanguage}`, () => { + postRequestSaveSearch(config); + + 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); + }); + }); + }); + }); +}; + +runSavedSearchTests(); diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/utils/saved_search.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/utils/saved_search.js new file mode 100644 index 000000000000..d95db5ef526e --- /dev/null +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/query_enhancements/utils/saved_search.js @@ -0,0 +1,494 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + DatasetTypes, + DATASOURCE_NAME, + END_TIME, + INDEX_PATTERN_WITH_TIME, + INDEX_WITH_TIME_1, + QueryLanguages, + START_TIME, + WORKSPACE_NAME, +} from '../../../../../../utils/apps/query_enhancements/constants'; + +const randomString = Math.random().toString(36); + +/** + * randomized workspace name + * @constant + * @type {string} + * @default + */ +export const workspaceName = `${WORKSPACE_NAME}-${randomString.substring(7)}`; + +/** + * randomized datasource name + * @constant + * @type {string} + * @default + */ +export const datasourceName = `${DATASOURCE_NAME}-${randomString.substring(0, 18)}`; + +/** + * The fields to select for saved search. Also takes shape of the API for saved search + * @constant + * @type {string[]} + * @default + */ +export const SELECTED_FIELD_COLUMNS = ['bytes_transferred', 'personal.name']; + +/** + * The field to sort for saved search. + * @constant + * @type {string} + * @default + */ +export const APPLIED_SORT = 'bytes_transferred'; + +/** + * The API shape of the sorted field for saved search + * @constant + * @type {string[][]} + * @default + */ +export const APPLIED_SORT_API = [[APPLIED_SORT, 'desc']]; + +/** + * The filter configuration to use for saved search + * @constant + * @type {{field: string, operator: string, value: string}} + * @default + */ +export const APPLIED_FILTERS = { + field: 'category', + operator: 'is one of', + value: 'Application', +}; + +/** + * Returns the query string to use for a given dataset+language + * @param {string} dataset - the dataset name to use + * @param {QueryEnhancementLanguage} language - the name of query language + * @returns {string} + */ +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}`); + } +}; + +/** + * Returns the expected hit count, if relevant, for the provided datasetType + language + * @param {QueryEnhancementDataset} datasetType - the type of the dataset + * @param {QueryEnhancementLanguage} language - the query language name + * @returns {number|undefined} + */ +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: + throw new Error(`getExpectedHitCount encountered unsupported datasetType: ${datasetType}`); + } +}; + +/** + * returns an array of data present in the results table to check against. This is used to ensure that sorting is working as expected + * @param {QueryEnhancementDataset} datasetType - the type of the dataset + * @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) => { + 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: + throw new Error(`getSampleTableData encountered unsupported datasetType: ${datasetType}`); + } +}; + +/** + * The configurations needed for saved search tests + * @typedef {Object} SavedSearchTestConfig + * @property {string} dataset - the dataset name to use + * @property {QueryEnhancementDataset} datasetType - the type of dataset + * @property {QueryEnhancementLanguage} language - the name of query language as it appears in the dashboard app + * @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 {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 + * @property {boolean} sort - whether the language supports sorting by fields + * @property {string} queryString - the query to use for saved search associated with the language + * @property {number|undefined} hitCount - the hitCount of the applied search config, if relevant + * @property {[[number,string]]|*[]} sampleTableData - an array of some table data to test against to ensure that sorting is working as expected + */ + +/** + * Returns the SavedSearchTestConfig for the provided dataset, datasetType, and language + * @param {string} dataset - the dataset name + * @param {QueryEnhancementDataset} datasetType - the type of the dataset + * @param {QueryEnhancementLanguageData} language - the relevant data for the query language to use + * @returns {SavedSearchTestConfig} + */ +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), + }; +}; + +/** + * Returns an array of test configurations for every query language + dataset permutation + * @returns {SavedSearchTestConfig[]} + */ +export 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); + }) + ); +}; + +/** + * Sets the top nav date if it is relevant for the passed language + * @param {QueryEnhancementLanguage} language - query language + */ +export const setDatePickerDatesAndSearchIfRelevant = (language) => { + if (language === QueryLanguages.SQL.name) { + return; + } + + cy.setTopNavDate(START_TIME, END_TIME); +}; + +/** + * Set the search configurations for the saved search + * @param {SavedSearchTestConfig} testConfig - the relevant config for the test case + */ +export const setSearchConfigurations = ({ + filters, + queryString, + histogram, + selectFields, + sort, +}) => { + 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'); + } + + if (selectFields) { + for (const field of SELECTED_FIELD_COLUMNS) { + cy.getElementByTestId(`fieldToggle-${field}`).click(); + } + + cy.getElementByTestId('querySubmitButton').should('be.visible'); + } + + if (sort) { + cy.getElementByTestId(`docTableHeaderFieldSort_${APPLIED_SORT}`).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_${APPLIED_SORT}`).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'); + } +}; + +/** + * Verify that the discover page is in the correct state after setSearchConfigurations have been run + * @param {SavedSearchTestConfig} testConfig - the relevant config for the test case + */ +export const verifyDiscoverPageState = ({ + dataset, + queryString, + language, + hitCount, + filters, + histogram, + selectFields, + sampleTableData, +}) => { + cy.getElementByTestId('datasetSelectorButton').contains(dataset); + 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 (selectFields) { + cy.getElementByTestId('docTableHeaderField').should('have.length', 3); + cy.getElementByTestId('docTableHeader-timestamp').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 + sampleTableData.forEach(([index, value]) => { + cy.getElementByTestId('osdDocTableCellDataField').eq(index).contains(value); + }); +}; + +/** + * After a saved search have been saved, verify the data in the assets page + * @param {SavedSearchTestConfig} testConfig - the relevant config for the test case + */ +export const verifySavedSearchInAssetsPage = ({ + apiLanguage, + dataset, + saveName, + queryString, + datasetType, + histogram, + selectFields, + sort, + filters, +}) => { + 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(saveName); + if (selectFields) { + expect(savedObjectAttributes.columns).eqls(SELECTED_FIELD_COLUMNS); + } + if (sort) { + expect(savedObjectAttributes.sort).eqls(APPLIED_SORT_API); + } + expect(searchSource).match( + // all special characters must be escaped + new RegExp(`"query":"${queryString.replaceAll(/([*{}])/g, (char) => `\\${char}`)}"`) + ); + expect(searchSource).match(new RegExp(`"language":"${apiLanguage}"`)); + expect(searchSource).match(new RegExp(`"title":"${dataset.replace('*', '\\*')}"`)); + expect(searchSource).match(new RegExp(`"type":"${datasetType}"`)); + + if (histogram) { + expect(searchSource).match(/"calendar_interval":"1w"/); + } + if (filters) { + expect(searchSource).match( + new RegExp(`"match_phrase":\{"${APPLIED_FILTERS.field}":"${APPLIED_FILTERS.value}"\}`) + ); + } + }); +}; + +/** + * Returns the API body that is needed when creating a saved search directly through an API call + * @param {SavedSearchTestConfig} config - language + dataset permutation configuration + * @param {string} workspaceId - workspace ID + * @param {string} datasourceId - datasource ID + * @param {string} indexPatternId - index pattern ID + * @returns {object} + */ +const getSavedObjectPostBody = (config, workspaceId, datasourceId, indexPatternId) => { + return { + attributes: { + title: config.saveName, + description: '', + hits: 0, + columns: config.selectFields ? SELECTED_FIELD_COLUMNS : undefined, + sort: config.sort ? APPLIED_SORT_API : 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, + }, + ], + workspaces: [workspaceId], + }; +}; + +/** + * send a POST request to API to create a saved search object + * @param {SavedSearchTestConfig} config - the relevant config for the test case + */ +export const postRequestSaveSearch = (config) => { + cy.get('@WORKSPACE_ID').then((workspaceId) => { + cy.get('@DATASOURCE_ID').then((datasourceId) => { + cy.get('@INDEX_PATTERN_ID').then((indexPatternId) => { + // 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, workspaceId, datasourceId, indexPatternId), + failOnStatusCode: false, + }); + }); + }); + }); +}; diff --git a/cypress/utils/apps/data_explorer/commands.js b/cypress/utils/apps/data_explorer/commands.js index 1e73e91584e3..4e7395692030 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 c3efc723ab1b..75f17a1d59ca 100644 --- a/cypress/utils/apps/data_explorer/index.d.ts +++ b/cypress/utils/apps/data_explorer/index.d.ts @@ -5,15 +5,20 @@ declare namespace Cypress { interface Chainable { - getTimeConfig(start: string, end: string): Chainable; - saveSearch(name: string): Chainable; - loadSaveSearch(name: string): Chainable; + verifyTimeConfig(start: string, end: 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 56ec83e71f08..2f95ef1b2bec 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 5c88339b6339..35bc746f8929 100644 --- a/cypress/utils/apps/query_enhancements/commands.js +++ b/cypress/utils/apps/query_enhancements/commands.js @@ -3,19 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -Cypress.Commands.add('setSingleLineQueryEditor', (value, submit = true) => { - const opts = { log: false }; - +Cypress.Commands.add('setQueryEditor', (value, opts = {}, submit = true) => { Cypress.log({ - name: 'setSingleLineQueryEditor', + name: 'setQueryEditor', displayName: 'set query', message: value, }); - cy.getElementByTestId('osdQueryEditor__singleLine', opts).type(value, opts); + // 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() + .focused() + .type('{ctrl}a') + .type('{backspace}') + .type('{meta}a') + .type('{backspace}') + .type(value, opts); if (submit) { - cy.updateTopNav(opts); + cy.updateTopNav({ log: false }); } }); @@ -34,6 +44,7 @@ Cypress.Commands.add('setQueryLanguage', (value) => { /** * Creates a new data source connection with basic auth + * It also saves the created data source's id to the alias @DATASOURCE_ID * @param {Object} options Configuration options for the data source * @param {string} options.name The name/title for the data source * @param {string} options.url The endpoint URL for the data source @@ -74,6 +85,8 @@ Cypress.Commands.add('addDataSource', (options) => { // Wait for successful creation cy.wait('@createDataSourceRequest').then((interception) => { expect(interception.response.statusCode).to.equal(200); + // save the created data source ID as an alias + cy.wrap(interception.response.body.id).as('DATASOURCE_ID'); }); // Verify redirect to data sources list page @@ -87,7 +100,7 @@ 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 @@ -95,6 +108,8 @@ Cypress.Commands.add('deleteDataSourceByName', (dataSourceName) => { cy.getElementByTestId('confirmModalConfirmButton').click(); }); +// Deletes all data sources. This command should only be used for convenience during development +// and should never be used in production Cypress.Commands.add('deleteAllDataSources', () => { cy.visit('app/dataSources'); cy.waitForLoader(true); @@ -123,3 +138,50 @@ Cypress.Commands.add('deleteAllDataSources', () => { } }); }); + +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/constants.js b/cypress/utils/apps/query_enhancements/constants.js index f85267e47d73..304fb902e2cb 100644 --- a/cypress/utils/apps/query_enhancements/constants.js +++ b/cypress/utils/apps/query_enhancements/constants.js @@ -4,7 +4,7 @@ */ export const DATASOURCE_NAME = 'data-logs-1'; -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_NAME = 'data_logs_small_time_1'; @@ -46,3 +46,112 @@ export const DS_API = { DELETE_DATA_SOURCE: `${DS_API_PREFIX}/data-source/`, }; export const DSM_API = '/internal/data-source-management/fetchDataSourceMetaData'; + +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_*'; + +/** + * The dataset type that saved search uses + * @typedef {('INDEXES'|'INDEX_PATTERN')} QueryEnhancementDataset + */ + +/** + * The languages that saved search uses + * @typedef {('DQL'|'Lucene'|'OpenSearch SQL'|'PPL')} QueryEnhancementLanguage + */ + +/** + * Describes discover operations that a given query language supports + * @typedef {Object} QueryEnhancementLanguageSupportedFeatures + * @property {boolean} filters - whether you can apply filters + * @property {boolean} histogram - whether the histogram appears + * @property {boolean} selectFields - whether you can select by specific fields to see the data + * @property {boolean} sort - whether you can sort the data by specific fields + */ + +/** + * Contains relevant data for a given Query Language + * @typedef {Object} QueryEnhancementLanguageData + * @property {QueryEnhancementLanguage} name - name of the language as it appears in the dashboard app + * @property {string} apiName - the name of the language recognized by the OpenSearch API + * @property {QueryEnhancementLanguageSupportedFeatures} supports - the list of operations supported by the language + */ + +/** + * Maps all the query languages that is supported by query enhancements to relevant data + * @property {QueryEnhancementLanguageData} DQL + * @property {QueryEnhancementLanguageData} Lucene + * @property {QueryEnhancementLanguageData} SQL + * @property {QueryEnhancementLanguageData} PPL + */ +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, + }, + }, +}; + +/** + * Contains relevant data for a given Dataset + * @typedef {Object} QueryEnhancementDatasetData + * @property {QueryEnhancementDataset} name - name of the dataset as recognized by the OpenSearch API + * @property {QueryEnhancementLanguage[]} supportedLanguages - an array of query languages that the dataset supports + */ + +/** + * Maps all the dataset that is supported by query enhancements to relevant data + * @type {Object.} + */ +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/utils/apps/query_enhancements/index.d.ts b/cypress/utils/apps/query_enhancements/index.d.ts index e2a97b0211cd..e8d415907c9b 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 { - setSingleLineQueryEditor(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; @@ -15,5 +19,16 @@ declare namespace Cypress { }): Chainable; deleteDataSourceByName(dataSourceName: string): Chainable; deleteAllDataSources(): 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/apps/workspace/commands.js b/cypress/utils/apps/workspace/commands.js index ac9d69f57aed..b0cbceaabd4e 100644 --- a/cypress/utils/apps/workspace/commands.js +++ b/cypress/utils/apps/workspace/commands.js @@ -6,9 +6,9 @@ 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); // wait until page loads cy.getElementByTestId('headerAppActionMenu').should('be.visible'); @@ -19,9 +19,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); // Check for toggleNavButton and handle accordingly // If collapsibleNavShrinkButton is shown which means toggleNavButton is already clicked, try clicking the app link directly @@ -46,7 +46,6 @@ Cypress.Commands.add( 'createWorkspaceIndexPatterns', (opts) => { const { - url, workspaceName, indexPattern, timefieldName, @@ -55,9 +54,12 @@ Cypress.Commands.add( isEnhancement = false, } = opts; + cy.intercept('POST', '/w/*/api/saved_objects/index-pattern').as( + 'createIndexPatternInterception' + ); + // Navigate to Workspace Specific IndexPattern Page cy.navigateToWorkSpaceSpecificPage({ - url, workspaceName, page: 'indexPatterns', isEnhancement, @@ -117,6 +119,12 @@ Cypress.Commands.add( } cy.getElementByTestId('createIndexPatternButton').should('be.visible').click(); + + cy.wait('@createIndexPatternInterception').then((interception) => { + // save the created index pattern ID as an alias + cy.wrap(interception.response.body.id).as('INDEX_PATTERN_ID'); + }); + cy.getElementByTestId('headerApplicationTitle').contains(indexPattern); } ); @@ -126,11 +134,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/apps/workspace/index.d.ts b/cypress/utils/apps/workspace/index.d.ts index 03343fdc9b58..d0ac13b5f5bb 100644 --- a/cypress/utils/apps/workspace/index.d.ts +++ b/cypress/utils/apps/workspace/index.d.ts @@ -7,17 +7,15 @@ declare namespace Cypress { interface Chainable { /** * Navigates to the workspace HomePage of a given workspace - * @param url - The base URL to navigate to * @param workspaceName - The name of the workspace to navigate to */ - navigateToWorkSpaceHomePage(url: string, workspaceName: string): Chainable; + navigateToWorkSpaceHomePage(workspaceName: string): Chainable; /** * Navigates to workspace specific pages * @param opts - Navigation options */ navigateToWorkSpaceSpecificPage(opts: { - url: string; workspaceName: string; page: string; isEnhancement?: boolean; @@ -25,10 +23,10 @@ declare namespace Cypress { /** * Creates an index pattern within the workspace using cluster + * It also saves the created index pattern id to the alias @INDEX_PATTERN_ID * @param opts - Index pattern creation options */ createWorkspaceIndexPatterns(opts: { - url: string; workspaceName: string; indexPattern: string; timefieldName: string; @@ -42,7 +40,6 @@ declare namespace Cypress { * @param opts - Index pattern deletion options */ deleteWorkspaceIndexPatterns(opts: { - url: string; workspaceName: string; indexPattern: string; isEnhancement?: boolean; diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index 51501f574ffa..cb9e2882e3c2 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -329,6 +329,8 @@ Cypress.Commands.add('deleteWorkspace', (workspaceName) => { }); Cypress.Commands.add('createInitialWorkspaceWithDataSource', (dataSourceTitle, workspaceName) => { + cy.intercept('POST', '/api/workspaces').as('createWorkspaceInterception'); + cy.getElementByTestId('workspace-initial-card-createWorkspace-button') .should('be.visible') .click(); @@ -347,6 +349,11 @@ Cypress.Commands.add('createInitialWorkspaceWithDataSource', (dataSourceTitle, w .trigger('click'); cy.getElementByTestId('workspace-detail-dataSources-associateModal-save-button').click(); cy.getElementByTestId('workspaceForm-bottomBar-createButton').should('be.visible').click(); + + cy.wait('@createWorkspaceInterception').then((interception) => { + // save the created workspace ID as an alias + cy.wrap(interception.response.body.result.id).as('WORKSPACE_ID'); + }); cy.contains(/successfully/); }); diff --git a/cypress/utils/index.d.ts b/cypress/utils/index.d.ts index bce04cf80697..3d0cb66a0daa 100644 --- a/cypress/utils/index.d.ts +++ b/cypress/utils/index.d.ts @@ -181,5 +181,14 @@ declare namespace Cypress { * cy.get('sourceSelector').drag('targetSelector') */ drag(targetSelector: string): Chainable; + + /** + * Creates workspace and attaches it to the provided data source + * It also saves the created workspace id as the alias @WORKSPACE_ID + */ + createInitialWorkspaceWithDataSource( + dataSourceTitle: string, + workspaceName: string + ): Chainable; } }