From c42e778037143f15ed438abb0d6a996c011321f6 Mon Sep 17 00:00:00 2001 From: Anan Date: Sun, 19 Jan 2025 00:34:55 -0800 Subject: [PATCH] Refactor TESTID 140 and resolve TESTID 46,47,49 Issue Resolved Partially https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8946 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8954 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8952 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8953 Signed-off-by: Anan --- .../field_display_filtering.spec.js | 307 +++++++++--------- .../query_enhancements/shared_links.spec.js | 237 ++++++++++++++ .../apps/query_enhancements/sidebar.spec.js | 226 +++++++++++++ .../utils/apps/query_enhancements/shared.js | 10 +- .../apps/query_enhancements/shared_links.js | 60 ++++ .../utils/apps/query_enhancements/sidebar.js | 89 +++++ .../utils/apps/query_enhancements/table.js | 90 +++++ 7 files changed, 862 insertions(+), 157 deletions(-) create mode 100644 cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/shared_links.spec.js create mode 100644 cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/sidebar.spec.js create mode 100644 cypress/utils/apps/query_enhancements/shared_links.js create mode 100644 cypress/utils/apps/query_enhancements/sidebar.js create mode 100644 cypress/utils/apps/query_enhancements/table.js diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js index a2281374dcc6..4ab903e2178a 100644 --- a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/field_display_filtering.spec.js @@ -8,7 +8,7 @@ import { INDEX_PATTERN_WITH_TIME, INDEX_WITH_TIME_1, } from '../../../../../utils/apps/constants'; -import * as dataExplorer from '../../../../../utils/apps/query_enhancements/field_display_filtering.js'; +import * as fieldFiltering from '../../../../../utils/apps/query_enhancements/field_display_filtering.js'; import { SECONDARY_ENGINE, BASE_PATH } from '../../../../../utils/constants'; import { NEW_SEARCH_BUTTON } from '../../../../../utils/dashboards/data_explorer/elements.js'; import { @@ -16,7 +16,6 @@ import { getRandomizedWorkspaceName, setDatePickerDatesAndSearchIfRelevant, } from '../../../../../utils/apps/query_enhancements/shared'; -import { generateFieldDisplayFilteringTestConfiguration } from '../../../../../utils/apps/query_enhancements/field_display_filtering'; const workspace = getRandomizedWorkspaceName(); @@ -65,162 +64,162 @@ describe('filter for value spec', () => { cy.deleteIndex(INDEX_WITH_TIME_1); }); - generateAllTestConfigurations(generateFieldDisplayFilteringTestConfiguration).forEach( - (config) => { - it(`filter actions in table field for ${config.testName}`, () => { - cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); - cy.setQueryLanguage(config.language); - setDatePickerDatesAndSearchIfRelevant(config.language); + generateAllTestConfigurations( + fieldFiltering.generateFieldDisplayFilteringTestConfiguration + ).forEach((config) => { + it(`filter actions in table field for ${config.testName}`, () => { + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); - cy.getElementByTestId('docTable').get('tbody tr').should('have.length.above', 3); // To ensure it waits until a full table is loaded into the DOM, instead of a bug where table only has 1 hit. + cy.getElementByTestId('docTable').get('tbody tr').should('have.length.above', 3); // To ensure it waits until a full table is loaded into the DOM, instead of a bug where table only has 1 hit. - const shouldText = config.isFilterButtonsEnabled ? 'exist' : 'not.exist'; - dataExplorer.getDocTableField(0, 0).within(() => { - cy.getElementByTestId('filterForValue').should(shouldText); - cy.getElementByTestId('filterOutValue').should(shouldText); - }); + const shouldText = config.isFilterButtonsEnabled ? 'exist' : 'not.exist'; + fieldFiltering.getDocTableField(0, 0).within(() => { + cy.getElementByTestId('filterForValue').should(shouldText); + cy.getElementByTestId('filterOutValue').should(shouldText); + }); + + if (config.isFilterButtonsEnabled) { + fieldFiltering.verifyDocTableFilterAction(0, 'filterForValue', '10,000', '1', true); + fieldFiltering.verifyDocTableFilterAction(0, 'filterOutValue', '10,000', '9,999', false); + } + }); - if (config.isFilterButtonsEnabled) { - dataExplorer.verifyDocTableFilterAction(0, 'filterForValue', '10,000', '1', true); - dataExplorer.verifyDocTableFilterAction(0, 'filterOutValue', '10,000', '9,999', false); + it(`filter actions in expanded table for ${config.testName}`, () => { + // Check if the first expanded Doc Table Field's first row's Filter For, Filter Out and Exists Filter buttons are disabled. + const verifyFirstExpandedFieldFilterForFilterOutFilterExistsButtons = () => { + const shouldText = config.isFilterButtonsEnabled ? 'be.enabled' : 'be.disabled'; + fieldFiltering.getExpandedDocTableRow(0, 0).within(() => { + cy.getElementByTestId('addInclusiveFilterButton').should(shouldText); + cy.getElementByTestId('removeInclusiveFilterButton').should(shouldText); + cy.getElementByTestId('addExistsFilterButton').should(shouldText); + }); + }; + + /** + * Check the Filter For or Out buttons in the expandedDocumentRowNumberth field in the expanded Document filters the correct value. + * @param {string} filterButton For or Out + * @param {number} docTableRowNumber Integer starts from 0 for the first row + * @param {number} expandedDocumentRowNumber Integer starts from 0 for the first row + * @param {string} expectedQueryHitsWithoutFilter expected number of hits in string after the filter is removed Note you should add commas when necessary e.g. 9,999 + * @param {string} expectedQueryHitsAfterFilterApplied expected number of hits in string after the filter is applied. Note you should add commas when necessary e.g. 9,999 + * @example verifyDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField('for', 0, 0, '10,000', '1'); + */ + const verifyDocTableFirstExpandedFieldFirstRowFilterForOutButtonFiltersCorrectField = ( + filterButton, + docTableRowNumber, + expandedDocumentRowNumber, + expectedQueryHitsWithoutFilter, + expectedQueryHitsAfterFilterApplied + ) => { + if (filterButton !== 'for' || filterButton !== 'out') { + cy.log('Filter button must be for or or.'); + return; } - }); - it(`filter actions in expanded table for ${config.testName}`, () => { - // Check if the first expanded Doc Table Field's first row's Filter For, Filter Out and Exists Filter buttons are disabled. - const verifyFirstExpandedFieldFilterForFilterOutFilterExistsButtons = () => { - const shouldText = config.isFilterButtonsEnabled ? 'be.enabled' : 'be.disabled'; - dataExplorer.getExpandedDocTableRow(0, 0).within(() => { - cy.getElementByTestId('addInclusiveFilterButton').should(shouldText); - cy.getElementByTestId('removeInclusiveFilterButton').should(shouldText); - cy.getElementByTestId('addExistsFilterButton').should(shouldText); + const filterButtonElement = + filterButton === 'for' ? 'addInclusiveFilterButton' : 'removeInclusiveFilterButton'; + const shouldText = filterButton === 'for' ? 'have.text' : 'not.have.text'; + + fieldFiltering + .getExpandedDocTableRowValue(docTableRowNumber, expandedDocumentRowNumber) + .then(($expandedDocumentRowValue) => { + const filterFieldText = $expandedDocumentRowValue.text(); + fieldFiltering + .getExpandedDocTableRow(docTableRowNumber, expandedDocumentRowNumber) + .within(() => { + cy.getElementByTestId(filterButtonElement).click(); + }); + // Verify pill text + cy.getElementByTestId('globalFilterLabelValue').should('have.text', filterFieldText); + cy.getElementByTestId('discoverQueryHits').should( + 'have.text', + expectedQueryHitsAfterFilterApplied + ); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. + fieldFiltering + .getExpandedDocTableRowValue(docTableRowNumber, expandedDocumentRowNumber) + .should(shouldText, filterFieldText); }); - }; - - /** - * Check the Filter For or Out buttons in the expandedDocumentRowNumberth field in the expanded Document filters the correct value. - * @param {string} filterButton For or Out - * @param {number} docTableRowNumber Integer starts from 0 for the first row - * @param {number} expandedDocumentRowNumber Integer starts from 0 for the first row - * @param {string} expectedQueryHitsWithoutFilter expected number of hits in string after the filter is removed Note you should add commas when necessary e.g. 9,999 - * @param {string} expectedQueryHitsAfterFilterApplied expected number of hits in string after the filter is applied. Note you should add commas when necessary e.g. 9,999 - * @example verifyDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField('for', 0, 0, '10,000', '1'); - */ - const verifyDocTableFirstExpandedFieldFirstRowFilterForOutButtonFiltersCorrectField = ( - filterButton, - docTableRowNumber, - expandedDocumentRowNumber, - expectedQueryHitsWithoutFilter, - expectedQueryHitsAfterFilterApplied - ) => { - if (filterButton !== 'for' || filterButton !== 'out') { - cy.log('Filter button must be for or or.'); - return; - } - - const filterButtonElement = - filterButton === 'for' ? 'addInclusiveFilterButton' : 'removeInclusiveFilterButton'; - const shouldText = filterButton === 'for' ? 'have.text' : 'not.have.text'; - - dataExplorer - .getExpandedDocTableRowValue(docTableRowNumber, expandedDocumentRowNumber) - .then(($expandedDocumentRowValue) => { - const filterFieldText = $expandedDocumentRowValue.text(); - dataExplorer - .getExpandedDocTableRow(docTableRowNumber, expandedDocumentRowNumber) - .within(() => { - cy.getElementByTestId(filterButtonElement).click(); - }); - // Verify pill text - cy.getElementByTestId('globalFilterLabelValue').should('have.text', filterFieldText); - cy.getElementByTestId('discoverQueryHits').should( - 'have.text', - expectedQueryHitsAfterFilterApplied - ); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. - dataExplorer - .getExpandedDocTableRowValue(docTableRowNumber, expandedDocumentRowNumber) - .should(shouldText, filterFieldText); - }); - cy.getElementByTestId('globalFilterBar').find('[aria-label="Delete"]').click(); - cy.getElementByTestId('discoverQueryHits').should( - 'have.text', - expectedQueryHitsWithoutFilter - ); - }; - - /** - * Check the first expanded Doc Table Field's first row's Exists Filter button filters the correct Field. - * @param {number} docTableRowNumber Integer starts from 0 for the first row - * @param {number} expandedDocumentRowNumber Integer starts from 0 for the first row - * @param {string} expectedQueryHitsWithoutFilter expected number of hits in string after the filter is removed Note you should add commas when necessary e.g. 9,999 - * @param {string} expectedQueryHitsAfterFilterApplied expected number of hits in string after the filter is applied. Note you should add commas when necessary e.g. 9,999 - */ - const verifyDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField = ( - docTableRowNumber, - expandedDocumentRowNumber, - expectedQueryHitsWithoutFilter, - expectedQueryHitsAfterFilterApplied - ) => { - dataExplorer - .getExpandedDocTableRowFieldName(docTableRowNumber, expandedDocumentRowNumber) - .then(($expandedDocumentRowField) => { - const filterFieldText = $expandedDocumentRowField.text(); - dataExplorer - .getExpandedDocTableRow(docTableRowNumber, expandedDocumentRowNumber) - .within(() => { - cy.getElementByTestId('addExistsFilterButton').click(); - }); - // Verify full pill text - // globalFilterLabelValue gives the inner element, but we may want all the text in the filter pill - cy.getElementByTestId('globalFilterLabelValue', { - timeout: 10000, - }) - .parent() - .should('have.text', filterFieldText + ': ' + 'exists'); - cy.getElementByTestId('discoverQueryHits').should( - 'have.text', - expectedQueryHitsAfterFilterApplied - ); - }); - cy.getElementByTestId('globalFilterBar').find('[aria-label="Delete"]').click(); - cy.getElementByTestId('discoverQueryHits').should( - 'have.text', - expectedQueryHitsWithoutFilter - ); - }; - - cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); - cy.setQueryLanguage(config.language); - setDatePickerDatesAndSearchIfRelevant(config.language); - - cy.getElementByTestId('docTable').get('tbody tr').should('have.length.above', 3); // To ensure it waits until a full table is loaded into the DOM, instead of a bug where table only has 1 hit. - dataExplorer.toggleDocTableRow(0); - verifyFirstExpandedFieldFilterForFilterOutFilterExistsButtons(); - dataExplorer.verifyDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); - - if (config.isFilterButtonsEnabled) { - verifyDocTableFirstExpandedFieldFirstRowFilterForOutButtonFiltersCorrectField( - 'for', - 0, - 0, - '10,000', - '1' - ); - verifyDocTableFirstExpandedFieldFirstRowFilterForOutButtonFiltersCorrectField( - 'out', - 0, - 0, - '10,000', - '9,999' - ); - verifyDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField( - 0, - 0, - '10,000', - '10,000' - ); - } - }); - } - ); + cy.getElementByTestId('globalFilterBar').find('[aria-label="Delete"]').click(); + cy.getElementByTestId('discoverQueryHits').should( + 'have.text', + expectedQueryHitsWithoutFilter + ); + }; + + /** + * Check the first expanded Doc Table Field's first row's Exists Filter button filters the correct Field. + * @param {number} docTableRowNumber Integer starts from 0 for the first row + * @param {number} expandedDocumentRowNumber Integer starts from 0 for the first row + * @param {string} expectedQueryHitsWithoutFilter expected number of hits in string after the filter is removed Note you should add commas when necessary e.g. 9,999 + * @param {string} expectedQueryHitsAfterFilterApplied expected number of hits in string after the filter is applied. Note you should add commas when necessary e.g. 9,999 + */ + const verifyDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField = ( + docTableRowNumber, + expandedDocumentRowNumber, + expectedQueryHitsWithoutFilter, + expectedQueryHitsAfterFilterApplied + ) => { + fieldFiltering + .getExpandedDocTableRowFieldName(docTableRowNumber, expandedDocumentRowNumber) + .then(($expandedDocumentRowField) => { + const filterFieldText = $expandedDocumentRowField.text(); + fieldFiltering + .getExpandedDocTableRow(docTableRowNumber, expandedDocumentRowNumber) + .within(() => { + cy.getElementByTestId('addExistsFilterButton').click(); + }); + // Verify full pill text + // globalFilterLabelValue gives the inner element, but we may want all the text in the filter pill + cy.getElementByTestId('globalFilterLabelValue', { + timeout: 10000, + }) + .parent() + .should('have.text', filterFieldText + ': ' + 'exists'); + cy.getElementByTestId('discoverQueryHits').should( + 'have.text', + expectedQueryHitsAfterFilterApplied + ); + }); + cy.getElementByTestId('globalFilterBar').find('[aria-label="Delete"]').click(); + cy.getElementByTestId('discoverQueryHits').should( + 'have.text', + expectedQueryHitsWithoutFilter + ); + }; + + cy.setDataset(config.dataset, DATASOURCE_NAME, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + + cy.getElementByTestId('docTable').get('tbody tr').should('have.length.above', 3); // To ensure it waits until a full table is loaded into the DOM, instead of a bug where table only has 1 hit. + fieldFiltering.toggleDocTableRow(0); + verifyFirstExpandedFieldFilterForFilterOutFilterExistsButtons(); + fieldFiltering.verifyDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + + if (config.isFilterButtonsEnabled) { + verifyDocTableFirstExpandedFieldFirstRowFilterForOutButtonFiltersCorrectField( + 'for', + 0, + 0, + '10,000', + '1' + ); + verifyDocTableFirstExpandedFieldFirstRowFilterForOutButtonFiltersCorrectField( + 'out', + 0, + 0, + '10,000', + '9,999' + ); + verifyDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField( + 0, + 0, + '10,000', + '10,000' + ); + } + }); + }); }); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/shared_links.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/shared_links.spec.js new file mode 100644 index 000000000000..83a5df5cdafc --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/shared_links.spec.js @@ -0,0 +1,237 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { START_TIME, END_TIME } from '../../../../../utils/apps/constants'; +import { INDEX_WITH_TIME_1, SECONDARY_ENGINE } from '../../../../../utils/constants'; +import { + getRandomizedWorkspaceName, + getRandomizedDatasourceName, + generateAllTestConfigurations, + setDatePickerDatesAndSearchIfRelevant, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { QueryLanguages } from '../../../../../utils/apps/query_enhancements/constants'; +import { selectFieldFromSidebar } from '../../../../../utils/apps/query_enhancements/sidebar'; +import { verifyShareUrl } from '../../../../../utils/apps/query_enhancements/shared_links'; +import { setSort } from '../../../../../utils/apps/query_enhancements/table'; + +const workspaceName = getRandomizedWorkspaceName(); +const datasourceName = getRandomizedDatasourceName(); + +const generateShareUrlsTestConfiguration = (dataset, datasetType, language) => { + const baseConfig = { + dataset, + datasetType, + language: language.name, + apiLanguage: language.apiName, + hasDocLinks: [QueryLanguages.DQL.name, QueryLanguages.Lucene.name].includes(language.name), + testName: `${language.name}-${datasetType}`, + saveName: `${language.name}-${datasetType}`, + }; + + return { + ...baseConfig, + }; +}; + +const getQueryString = (config) => { + if (config.language === QueryLanguages.DQL.name) { + return 'bytes_transferred > 9950'; + } + if (config.language === QueryLanguages.Lucene.name) { + return 'bytes_transferred: {9950 TO *}'; + } + if (config.language === QueryLanguages.SQL.name) { + return `SELECT * FROM ${config.dataset} WHERE bytes_transferred > 9950`; + } + return `source = ${config.dataset} | where bytes_transferred > 9950`; +}; + +export const runSharedLinksTests = () => { + describe('discover sharing tests', () => { + const testData = { + fields: ['service_endpoint'], + sort: ['asc'], + interval: 'w', + filter: ['category', 'Network'], + }; + beforeEach(() => { + cy.setupTestData( + SECONDARY_ENGINE.url, + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] + ); + cy.addDataSource({ + name: datasourceName, + url: SECONDARY_ENGINE.url, + authType: 'no_auth', + }); + //cy.deleteWorkspaceByName(`${workspaceName}`); + cy.visit('/app/home'); + cy.createInitialWorkspaceWithDataSource(datasourceName, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(`${workspaceName}`); + cy.deleteDataSourceByName(datasourceName); + cy.deleteIndex('data_logs_small_time_1'); + cy.window().then((win) => { + win.localStorage.clear(); + win.sessionStorage.clear(); + }); + }); + + generateAllTestConfigurations(generateShareUrlsTestConfiguration, { + indexPattern: 'data_logs_small_time_1*', + index: 'data_logs_small_time_1', + }).forEach((config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === 'INDEX_PATTERN') { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: 'data_logs_small_time_1', + timefieldName: 'timestamp', + dataSource: datasourceName, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + }); + + const queryString = getQueryString(config); + + it(`should handle shared document links correctly for ${config.testName}`, () => { + // Setup + cy.setDataset(config.dataset, datasourceName, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + + if (config.hasDocLinks) { + // Test surrounding documents link + cy.get('tbody tr') + .first() + .find('[data-test-subj="docTableExpandToggleColumn"] button') + .click(); + + cy.getElementByTestId('docTableRowAction-0') + .should('exist') + .and('contain.text', 'View surrounding documents') + .invoke('removeAttr', 'target') + .click(); + cy.url().should('include', '/context/'); + cy.go('back'); + + // Test single document link + cy.get('tbody tr') + .first() + .find('[data-test-subj="docTableExpandToggleColumn"] button') + .click(); + + cy.getElementByTestId('docTableRowAction-1') + .should('exist') + .and('contain.text', 'View single document') + .invoke('removeAttr', 'target') + .click(); + cy.url().should('include', '/doc/'); + cy.go('back'); + } else { + // Verify no document links for SQL/PPL + cy.get('tbody tr') + .first() + .find('[data-test-subj="docTableExpandToggleColumn"] button') + .click(); + cy.getElementByTestId('docTableRowAction-0').should('not.exist'); + cy.getElementByTestId('docTableRowAction-1').should('not.exist'); + } + }); + + it(`should persist state in shared links for ${config.testName}`, () => { + // Set dataset and language + cy.setDataset(config.dataset, datasourceName, config.datasetType); + cy.setQueryLanguage(config.language); + + // Set time range + if (config.language !== QueryLanguages.SQL.name) { + cy.setTopNavDate(START_TIME, END_TIME); + cy.getElementByTestId('discoverIntervalSelect').select(testData.interval); + } + + // Set query + cy.setQueryEditor(queryString, { parseSpecialCharSequences: false }); + + // Set filter for DQL/Lucene + if (config.hasDocLinks) { + cy.submitFilterFromDropDown(testData.filter[0], 'is', testData.filter[1], true); + } + + // Add fields from side panel + testData.fields.forEach((field, i) => { + selectFieldFromSidebar(field); + if (config.hasDocLinks) { + setSort(field, testData.sort[i]); + } + }); + + // Test snapshot url + cy.getElementByTestId('shareTopNavButton').click(); + cy.getElementByTestId('copyShareUrlButton') + .invoke('attr', 'data-share-url') + .then((url) => { + verifyShareUrl(url, config, testData, datasourceName, queryString); + }); + + // Test short url + cy.getElementByTestId('useShortUrl').click(); + // Need to wait for short url to generate + cy.wait(2000); + cy.getElementByTestId('copyShareUrlButton') + .invoke('attr', 'data-share-url') + .then((shareUrl) => { + cy.log('Got share URL:', shareUrl); + return cy.request({ + url: shareUrl, + followRedirect: false, + }); + }) + .then((response) => { + cy.log('Response details:', JSON.stringify(response, null, 2)); + const redirectUrl = response.headers.location; + cy.log('Redirect URL:', redirectUrl); + verifyShareUrl(redirectUrl, config, testData, datasourceName, queryString); + }); + + // Test saved object url + // Before save, export as saved object is disabled + cy.getElementByTestId('exportAsSavedObject').find('input').should('be.disabled'); + cy.saveSearch(config.saveName); + cy.waitForLoader(true); + cy.getElementByTestId('shareTopNavButton').click(); + cy.getElementByTestId('exportAsSavedObject').find('input').should('not.be.disabled'); + cy.getElementByTestId('exportAsSavedObject').click(); + // Get saved search ID + cy.url().then((url) => { + const viewMatch = url.match(/\/view\/([^?#]+)/); + const savedSearchId = viewMatch ? viewMatch[1] : ''; + + // Verify ID exists and is properly formatted + expect(savedSearchId).to.not.be.empty; + + cy.getElementByTestId('copyShareUrlButton') + .invoke('attr', 'data-share-url') + .then((shareUrl) => { + expect(shareUrl).to.include(`/view/${savedSearchId}`); + }); + }); + }); + }); + }); + }); +}; + +runSharedLinksTests(); diff --git a/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/sidebar.spec.js b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/sidebar.spec.js new file mode 100644 index 000000000000..be9257bf244f --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/opensearch_dashboards/apps/query_enhancements/sidebar.spec.js @@ -0,0 +1,226 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { INDEX_WITH_TIME_1, SECONDARY_ENGINE } from '../../../../../utils/constants'; +import { + generateAllTestConfigurations, + getRandomizedWorkspaceName, + getRandomizedDatasourceName, + setDatePickerDatesAndSearchIfRelevant, +} from '../../../../../utils/apps/query_enhancements/shared'; +import { getDocTableField } from '../../../../../utils/apps/query_enhancements/field_display_filtering'; +import * as sideBar from '../../../../../utils/apps/query_enhancements/sidebar'; +import { generateSavedTestConfiguration } from '../../../../../utils/apps/query_enhancements/saved'; + +const workspaceName = getRandomizedWorkspaceName(); +const datasourceName = getRandomizedDatasourceName(); + +const addSidebarFieldsAndCheckDocTableColumns = ( + testFields, + expectedValues, + pplQuery, + sqlQuery, + isIndexPattern, + config +) => { + // Helper functions + const getDocTableHeaderByIndex = (index) => + cy.getElementByTestId('docTableHeaderField').eq(index); + + const checkTableHeaders = (headers) => { + headers.forEach((header, index) => { + getDocTableHeaderByIndex(index + 1).should('have.text', header); + }); + }; + + const checkDocTableColumn = (values, columnNumber) => { + values.forEach((value, index) => { + getDocTableField(columnNumber, index).should('have.text', value); + }); + }; + + // Test steps + cy.wrap([ + () => getDocTableHeaderByIndex(1).should('have.text', '_source'), + () => { + testFields.forEach((field) => { + sideBar.selectFieldFromSidebar(field); + }); + getDocTableHeaderByIndex(1).should('not.have.text', '_source'); + checkTableHeaders(testFields); + }, + () => { + testFields.slice(0, 2).forEach((field) => { + sideBar.selectFieldFromSidebar(field); + }); + getDocTableHeaderByIndex(1).should('not.have.text', testFields[0]); + getDocTableHeaderByIndex(2).should('not.have.text', testFields[1]); + testFields.slice(2).forEach((field) => { + sideBar.selectFieldFromSidebar(field); + }); + getDocTableHeaderByIndex(1).should('have.text', '_source'); + getDocTableHeaderByIndex(2).should('not.exist'); + testFields.forEach((field) => { + sideBar.selectFieldFromSidebar(field); + }); + getDocTableHeaderByIndex(1).should('not.have.text', '_source'); + checkTableHeaders(testFields); + }, + ]).each((fn) => fn()); + + if (isIndexPattern && config.language !== 'OpenSearch SQL') { + cy.getElementByTestId('discoverQueryHits').should('have.text', '10,000'); + } + + if (config.language === 'PPL') { + cy.intercept('**/api/enhancements/search/ppl').as('query'); + cy.setQueryEditor(pplQuery); + cy.wait('@query').then(() => { + checkTableHeaders(testFields); + if (isIndexPattern) { + cy.getElementByTestId('discoverQueryHits').should('have.text', '1,152'); + } + checkDocTableColumn(expectedValues, 2); + }); + } else if (config.language === 'OpenSearch SQL') { + cy.intercept('**/api/enhancements/search/sql').as('query'); + cy.setQueryEditor(sqlQuery); + cy.wait('@query').then(() => { + checkTableHeaders(testFields); + checkDocTableColumn(expectedValues, 2); + }); + } else if (config.language === 'DQL' || config.language === 'Lucene') { + checkTableHeaders(testFields); + } +}; + +const checkFilteredFieldsForAllLanguages = () => { + const searchValues = [ + { search: '_index', assertion: 'equal' }, + { search: ' ', assertion: null }, + { search: 'a', assertion: 'include' }, + { search: 'age', assertion: 'include' }, + { search: 'non-existent field', assertion: null }, + ]; + + searchValues.forEach(({ search, assertion }) => { + sideBar.checkSidebarFilterBarResults(search, assertion); + }); +}; + +const checkSidebarPanelBehavior = () => { + const checkPanelVisibility = (shouldBeVisible) => { + cy.getElementByTestId('sidebarPanel').should(shouldBeVisible ? 'be.visible' : 'not.be.visible'); + }; + + checkPanelVisibility(true); + sideBar.clickSidebarCollapseBtn(); + checkPanelVisibility(false); + sideBar.clickSidebarCollapseBtn(false); + checkPanelVisibility(true); +}; + +export const runSideBarTests = () => { + describe('sidebar spec', () => { + const testData = { + pplQuery: (dataset) => `source = ${dataset} | where status_code = 200`, + sqlQuery: (dataset) => `SELECT * FROM ${dataset} WHERE status_code = 200`, + simpleFields: { + fields: ['service_endpoint', 'response_time', 'bytes_transferred', 'request_url'], + expectedValues: ['3.32', '2.8', '3.35', '1.68', '4.98'], + }, + nestedFields: { + fields: ['personal.name', 'personal.age', 'personal.birthdate', 'personal.address.country'], + expectedValues: ['28', '55', '76', '56', '36'], + }, + }; + + beforeEach(() => { + cy.setupTestData( + SECONDARY_ENGINE.url, + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.mapping.json`], + [`cypress/fixtures/query_enhancements/data_logs_1/${INDEX_WITH_TIME_1}.data.ndjson`] + ); + cy.addDataSource({ + name: datasourceName, + url: SECONDARY_ENGINE.url, + authType: 'no_auth', + }); + cy.deleteWorkspaceByName(`${workspaceName}`); + cy.visit('/app/home'); + cy.createInitialWorkspaceWithDataSource(datasourceName, workspaceName); + }); + + afterEach(() => { + cy.deleteWorkspaceByName(`${workspaceName}`); + cy.deleteDataSourceByName(datasourceName); + cy.deleteIndex('data_logs_small_time_1'); + cy.window().then((win) => { + win.localStorage.clear(); + win.sessionStorage.clear(); + }); + }); + + generateAllTestConfigurations(generateSavedTestConfiguration, { + indexPattern: 'data_logs_small_time_1*', + index: 'data_logs_small_time_1', + }).forEach((config) => { + describe(`${config.testName}`, () => { + beforeEach(() => { + if (config.datasetType === 'INDEX_PATTERN') { + cy.createWorkspaceIndexPatterns({ + workspaceName: workspaceName, + indexPattern: 'data_logs_small_time_1', + timefieldName: 'timestamp', + dataSource: datasourceName, + isEnhancement: true, + }); + } + cy.navigateToWorkSpaceSpecificPage({ + workspaceName: workspaceName, + page: 'discover', + isEnhancement: true, + }); + cy.setDataset(config.dataset, datasourceName, config.datasetType); + cy.setQueryLanguage(config.language); + setDatePickerDatesAndSearchIfRelevant(config.language); + sideBar.removeAllSelectedFields(); + }); + + it('adds simple fields', () => { + addSidebarFieldsAndCheckDocTableColumns( + testData.simpleFields.fields, + testData.simpleFields.expectedValues, + testData.pplQuery(config.dataset), + testData.sqlQuery(config.dataset), + config.datasetType === 'INDEX_PATTERN', + config + ); + }); + + it('adds nested fields', () => { + addSidebarFieldsAndCheckDocTableColumns( + testData.nestedFields.fields, + testData.nestedFields.expectedValues, + testData.pplQuery(config.dataset), + testData.sqlQuery(config.dataset), + config.datasetType === 'INDEX_PATTERN', + config + ); + }); + + it('filters fields correctly', () => { + checkFilteredFieldsForAllLanguages(); + }); + + it('handles panel collapse/expand correctly', () => { + checkSidebarPanelBehavior(); + }); + }); + }); + }); +}; + +runSideBarTests(); diff --git a/cypress/utils/apps/query_enhancements/shared.js b/cypress/utils/apps/query_enhancements/shared.js index 9fe5a24512f4..e7a5e8436182 100644 --- a/cypress/utils/apps/query_enhancements/shared.js +++ b/cypress/utils/apps/query_enhancements/shared.js @@ -46,18 +46,22 @@ export const getRandomizedDatasourceName = () => /** * Returns an array of test configurations for every query language + dataset permutation * @param {GenerateTestConfigurationCallback} generateTestConfigurationCallback - cb function that generates a test case for the particular permutation + * @param {Object} [options] - Optional configuration options + * @param {string} [options.indexPattern] - Custom index pattern name (defaults to INDEX_PATTERN_WITH_TIME) + * @param {string} [options.index] - Custom index name (defaults to INDEX_WITH_TIME_1) * @returns {object[]} */ -export const generateAllTestConfigurations = (generateTestConfigurationCallback) => { +export const generateAllTestConfigurations = (generateTestConfigurationCallback, options = {}) => { + const { indexPattern = INDEX_PATTERN_WITH_TIME, index = INDEX_WITH_TIME_1 } = options; 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; + datasetToUse = indexPattern; break; case DatasetTypes.INDEXES.name: - datasetToUse = INDEX_WITH_TIME_1; + datasetToUse = index; break; default: throw new Error( diff --git a/cypress/utils/apps/query_enhancements/shared_links.js b/cypress/utils/apps/query_enhancements/shared_links.js new file mode 100644 index 000000000000..dc32beb2a636 --- /dev/null +++ b/cypress/utils/apps/query_enhancements/shared_links.js @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { START_TIME, END_TIME, QueryLanguages } from './constants'; + +const formatDateForUrl = (dateString) => { + const date = new Date(dateString); + return date.toISOString(); +}; + +/** + * Verifies share URL parameters based on query language + * @param {string} url Share URL to verify + * @param {Object} config Test configuration + * @param {Object} testData Data that should be in columns + * @param {string} datasourceName Expected datasource name + */ +export const verifyShareUrl = (url, config, testData, datasourceName, queryString) => { + const hashPart = url.split('#')[1]; + if (!hashPart) { + throw new Error('No hash part in URL'); + } + + const searchParams = new URLSearchParams(hashPart); + const q = searchParams.get('_q'); + const a = searchParams.get('_a'); + const g = searchParams.get('_g'); + + // Query param checks + expect(q).to.include(datasourceName); + expect(q).to.include(config.dataset); + expect(q).to.include(config.datasetType); + expect(q).to.include(queryString); + if (config.language === QueryLanguages.SQL.name) { + // Not OpenSearch SQL + expect(q).to.include('language:SQL'); + } else if (config.language === QueryLanguages.PPL.name) { + expect(q).to.include(`language:${config.language}`); + } else { + const expectedLanguage = config.language === QueryLanguages.DQL.name ? 'kuery' : 'lucene'; + expect(q).to.include(`language:${expectedLanguage}`); + expect(q).to.include(`${testData.filter[0]}:${testData.filter[1]}`); + } + + // App state checks + testData.fields.forEach((field, i) => { + expect(a).to.include(field); + if ([QueryLanguages.DQL.name, QueryLanguages.Lucene.name].includes(config.language)) { + expect(a).to.include(`${field},${testData.sort[i]}`); + expect(a).to.include(`interval:${testData.interval}`); + } + }); + + // Global state check + if (config.language !== QueryLanguages.SQL.name) { + expect(g).to.include(`from:'${formatDateForUrl(START_TIME)}'`); + expect(g).to.include(`to:'${formatDateForUrl(END_TIME)}'`); + } +}; diff --git a/cypress/utils/apps/query_enhancements/sidebar.js b/cypress/utils/apps/query_enhancements/sidebar.js new file mode 100644 index 000000000000..34bf630d057b --- /dev/null +++ b/cypress/utils/apps/query_enhancements/sidebar.js @@ -0,0 +1,89 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Click on the sidebar collapse button. + * @param {boolean} collapse true for collapsing, false for expanding + */ +export const clickSidebarCollapseBtn = (collapse = true) => { + if (collapse) { + cy.getElementByTestId('euiResizableButton').trigger('mouseover').click(); + } + cy.get('.euiResizableToggleButton').click({ force: true }); +}; + +/** + * Check the results of the sidebar filter bar search. + * @param {string} search text to look up + * @param {string} assertion the type of assertion that is going to be performed. Example: 'eq', 'include'. If an assertion is not passed, a negative test is performend. + */ +export const checkSidebarFilterBarResults = (search, assertion) => { + cy.getElementByTestId('fieldFilterSearchInput').type(search, { force: true }); + if (assertion) { + // Get all sidebar fields and iterate over all of them + cy.get('[data-test-subj^="field-"]:not([data-test-subj$="showDetails"])').each(($field) => { + cy.wrap($field) + .should('be.visible') + .invoke('text') + .then(($fieldTxt) => { + cy.wrap($fieldTxt).should(assertion, search); + }); + }); + } else { + // No match should be found + cy.get('[data-test-subj^="field-"]:not([data-test-subj$="showDetails"])').should('not.exist'); + } + cy.get('button[aria-label="Clear input"]').click(); +}; + +/** + * Removes all currently selected fields from the sidebar + */ +export const removeAllSelectedFields = () => { + cy.get('[data-test-subj="fieldList-selected"]').then(($list) => { + if ($list.find('[data-test-subj^="field-"]').length > 0) { + // Remove all selected fields + $list.find('[data-test-subj^="fieldToggle-"]').each((_, el) => { + cy.wrap(el).click(); + }); + } + }); +}; + +/** + * Toggles field visibility in sidebar by clicking field name + * @param {string} field Field name to toggle + * @example + * // Show/hide timestamp field in sidebar + * selectFieldFromSidebar('timestamp') + */ +export const selectFieldFromSidebar = (field) => { + cy.getElementByTestId(`fieldToggle-${field}`).click(); +}; + +/** + * The configurations needed for side bar tests + * @typedef {Object} SideBarTestConfig + * @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} testName - the phrase to add to the test case's title + */ + +/** + * Returns the SideBarTestConfig 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 {SideBarTestConfig} + */ +export const generateSideBarTestConfiguration = (dataset, datasetType, language) => { + return { + dataset, + datasetType, + language: language.name, + testName: `dataset: ${datasetType} and language: ${language.name}`, + }; +}; diff --git a/cypress/utils/apps/query_enhancements/table.js b/cypress/utils/apps/query_enhancements/table.js new file mode 100644 index 000000000000..1701de8de1ce --- /dev/null +++ b/cypress/utils/apps/query_enhancements/table.js @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Checks if a field is sortable by looking for sort button + * @param {string} field Field name to check sortability + * @returns {boolean} True if field has sort button, false otherwise + * @example + * // Check if timestamp field is sortable + * if (isSortable('timestamp')) { + * // Perform sort operations + * } + */ +export const isSortable = (field) => { + cy.getElementByTestId(`docTableHeaderFieldSort_${field}`).should('exist'); +}; + +/** + * Sets the sort direction for a field in the doc table + * @param {string} field - The field name to sort on + * @param {'asc'|'desc'|'none'} direction - Sort direction: + * - 'asc': Sort ascending + * - 'desc': Sort descending + * - 'none': Remove sort + * @example + * // Sort timestamp ascending + * setSort('timestamp', 'asc') + */ +export const setSort = (field, direction) => { + cy.getElementByTestId(`docTableHeaderFieldSort_${field}`) + .should('exist') + .then(($btn) => { + const getCurrentDir = () => { + return $btn.attr('aria-label').toLowerCase(); + }; + + const getActualDir = (label) => { + if (label.includes('ascending')) return 'desc'; + if (label.includes('descending')) return 'asc'; + return 'none'; + }; + + let attempts = 0; + const maxAttempts = 2; + + const clickUntilDesired = () => { + const currentDir = getActualDir(getCurrentDir()); + if (currentDir !== direction && attempts < maxAttempts) { + attempts++; + cy.getElementByTestId(`docTableHeaderFieldSort_${field}`).click(); + cy.getElementByTestId(`docTableHeaderFieldSort_${field}`) + .should('exist') + .then(($newBtn) => { + if (getActualDir($newBtn.attr('aria-label').toLowerCase()) !== direction) { + clickUntilDesired(); + } + }); + } + }; + + clickUntilDesired(); + }); +}; + +/** + * Gets the current sort direction of a field + * @param {string} field Field name to check sort direction + * @returns {'asc'|'desc'|'none'|null} Current sort direction, or null if field not sortable + * @example + * // Get current sort direction of timestamp field + * const direction = getSort('timestamp') // Returns 'asc', 'desc', 'none', or null + */ +export const getSort = (field) => { + if (!isSortable(field)) { + return null; + } + + return cy + .getElementByTestId(`docTableHeaderFieldSort_${field}`) + .invoke('attr', 'aria-label') + .then((label) => { + // Convert aria-label's next state to current state + const labelLower = label.toLowerCase(); + if (labelLower.includes('ascending')) return 'desc'; + if (labelLower.includes('descending')) return 'asc'; + return 'none'; + }); +};