From 63e0bc43dd7986aa15d7a560542249a25ad7a5fa Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Thu, 4 Aug 2022 13:39:40 -0700 Subject: [PATCH] [D&D] Initial functional tests (#2070) * fix: searchable dropdown Signed-off-by: Ashwin Pc * fix: broken empty test Signed-off-by: Ashwin Pc * test(FTR): Adds basic functional tests for D&D Signed-off-by: Ashwin Pc * test(FTR): Adds CI group 13 to test workflow Signed-off-by: Ashwin Pc * fix: nit fixes Signed-off-by: Ashwin Pc * chore: att test to jenkinsfile Signed-off-by: Ashwin Pc * chore: minor nit fixes Signed-off-by: Ashwin Pc --- .github/workflows/build_and_test_workflow.yml | 2 +- Jenkinsfile | 1 + TESTING.md | 17 ++- .../components/data_tab/dropbox.tsx | 10 +- .../application/components/data_tab/title.tsx | 4 +- .../components/searchable_dropdown.scss | 43 ++++--- .../components/searchable_dropdown.tsx | 2 + .../application/components/workspace.tsx | 4 +- .../metric/to_expression.test.ts | 6 - test/common/config.js | 1 + test/examples/embeddables/dashboard.ts | 2 +- test/functional/apps/wizard/_base.ts | 48 ++++++++ .../apps/wizard/_experimental_vis.ts | 51 ++++++++ test/functional/apps/wizard/index.ts | 39 ++++++ test/functional/config.js | 5 + test/functional/page_objects/index.ts | 2 + test/functional/page_objects/wizard_page.ts | 113 ++++++++++++++++++ test/mocha_decorations.d.ts | 3 +- .../test_suites/panel_actions/index.js | 5 +- 19 files changed, 325 insertions(+), 33 deletions(-) delete mode 100644 src/plugins/wizard/public/visualizations/metric/to_expression.test.ts create mode 100644 test/functional/apps/wizard/_base.ts create mode 100644 test/functional/apps/wizard/_experimental_vis.ts create mode 100644 test/functional/apps/wizard/index.ts create mode 100644 test/functional/page_objects/wizard_page.ts diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index d82d0f45bb04..5a5b2a3ef2ae 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -75,7 +75,7 @@ jobs: name: Run functional tests strategy: matrix: - group: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ] + group: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] steps: - run: echo Running functional tests for ciGroup${{ matrix.group }} diff --git a/Jenkinsfile b/Jenkinsfile index c221237aeb93..2b967bc59d01 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,6 +68,7 @@ def functionalDynamicParallelSteps(image){ "ciGroup10", "ciGroup11", "ciGroup12", + "ciGroup13", ] for (int i = 0; i < ciGroups.size(); i++) { def currentCiGroup = ciGroups[i]; diff --git a/TESTING.md b/TESTING.md index 15c2792d3307..62a2dfd53956 100644 --- a/TESTING.md +++ b/TESTING.md @@ -50,7 +50,7 @@ To run specific functional tests, you can run by CI group: To debug functional tests: Say that you would want to debug a test in CI group 1, you can run the following command in your environment: -`node --debug-brk --inspect scripts/functional_tests.js --config test/functional/config.js --include ciGroup1 --debug` +`node --inspect-brk --inspect scripts/functional_tests.js --config test/functional/config.js --include ciGroup1 --debug` This will print off an address, to which you could open your chrome browser on your instance and navigate to `chrome://inspect/#devices` and inspect the functional test runner `scripts/functional_tests.js`. @@ -89,9 +89,24 @@ Automated testing is provided with Jenkins for Continuous Integration. Jenkins e Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable: `export TEST_BROWSER_HEADLESS=1` +Since local Selenium tests are run in a real browser, the dev environment should have a desktop environment and Google Chrome or Chromium installed to run the tests. + By default the version of OpenSearch Dashboards will pull the snapshot of the same version of OpenSearch if available while running a few integration tests and for running functional tests. However, if the version of OpenSearch Dashboards is not available, you can build OpenSearch locally and point the functional test runner to the executable with: `export TEST_OPENSEARCH_FROM=[local directory of OpenSearch executable]` +Selinium tests require a chromedriver and a corresponding version of chrome to run properly. Depending on the version of chromedriver used, you may need to use a version of Google Chrome that is not the latest version. You can do this by running: + +```sh +# Enter the version of chrome that you want to install +CHROME_VERSION=100.0.4896.127-1 + +# Download Chrome to a temp directory +curl -sSL "https://dl.google.com/linux/linux_signing_key.pub" | sudo apt-key add - && wget -O /tmp/chrome.deb "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb" + +# Install/Downgrade Chrome +sudo apt-get install -y --allow-downgrades /tmp/chrome.deb +``` + # Misc Although Jest is the standard for this project, there are a few Mocha tests that still exist. You can run these tests by running: `yarn test:mocha` diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx index 9c6c9e290f8c..f6b7a6ca221b 100644 --- a/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx @@ -96,7 +96,12 @@ const DropboxComponent = ({ draggableId={id} index={index} > - + onEditField(id)}> {label} @@ -108,6 +113,7 @@ const DropboxComponent = ({ aria-label="clear-field" iconSize="s" onClick={() => animateDelete(id)} + data-test-subj="dropBoxRemoveBtn" /> @@ -115,6 +121,7 @@ const DropboxComponent = ({ {fields.length < limit && ( onAddField()} + data-test-subj="dropBoxAddBtn" /> )} diff --git a/src/plugins/wizard/public/application/components/data_tab/title.tsx b/src/plugins/wizard/public/application/components/data_tab/title.tsx index 1f48db369669..8083efd05b0b 100644 --- a/src/plugins/wizard/public/application/components/data_tab/title.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/title.tsx @@ -18,7 +18,9 @@ export interface TitleProps { } export const Title = ({ title, isSecondary, closeMenu }: TitleProps) => { - const icon = isSecondary && ; + const icon = isSecondary && ( + + ); return ( <>
diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss index 59f9771b35ac..de03454dffbe 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.scss +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -6,25 +6,34 @@ .searchableDropdown { overflow: "hidden"; -} -.searchableDropdown .euiPopover, -.searchableDropdown .euiPopover__anchor { - width: 100%; -} + .euiFormControlLayout__childrenWrapper { + display: flex; + } -.searchableDropdown--fixedWidthChild { - width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2); -} + &--topDisplay { + padding-right: $euiSizeL; + font-size: $euiFontSizeS; + flex-grow: 1; -.searchableDropdown--topDisplay { - padding-right: $euiSizeL; - font-size: $euiFontSizeS; -} + .euiButtonEmpty__content { + justify-content: flex-start; + } + } + + &--fixedWidthChild { + width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2); + } + + &--selectableWrapper .euiSelectableList { + // When clicking on the selectable content it will "highlight" itself with a box shadow + // This turns that off + box-shadow: none !important; + margin: ($euiFormControlPadding * -1) - 4; + } -.searchableDropdown--selectableWrapper .euiSelectableList { - // When clicking on the selectable content it will "highlight" itself with a box shadow - // This turns that off - box-shadow: none !important; - margin: ($euiFormControlPadding * -1) - 4; + .euiPopover, + .euiPopover__anchor { + width: 100%; + } } diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx index 0d489b818167..3ff8300e8d48 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx @@ -118,6 +118,7 @@ export const SearchableDropdown = ({
{selectedText} diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index ab320c8257ad..087cb656c622 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -89,11 +89,11 @@ export const Workspace: FC = ({ children }) => { - + {expression ? ( ) : ( - + Add a field to start} body={ diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts deleted file mode 100644 index 9fd364ad256c..000000000000 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// TODO: Cleanup the TODOs in './to_expression.ts' before writing tests for this function diff --git a/test/common/config.js b/test/common/config.js index 5db5748087a3..26abcc2fa586 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -78,6 +78,7 @@ export default function () { `--opensearchDashboards.branding.mark.defaultUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg`, `--opensearchDashboards.branding.mark.darkModeUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg`, `--opensearchDashboards.branding.applicationTitle=OpenSearch`, + `--wizard.enabled=true`, ], }, services, diff --git a/test/examples/embeddables/dashboard.ts b/test/examples/embeddables/dashboard.ts index 2dac8c05849e..434bb9cb69cc 100644 --- a/test/examples/embeddables/dashboard.ts +++ b/test/examples/embeddables/dashboard.ts @@ -30,7 +30,7 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; -export const testDashboardInput = { +const testDashboardInput = { panels: { '1': { gridData: { diff --git a/test/functional/apps/wizard/_base.ts b/test/functional/apps/wizard/_base.ts new file mode 100644 index 000000000000..99cce7fce489 --- /dev/null +++ b/test/functional/apps/wizard/_base.ts @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'wizard']); + const log = getService('log'); + const retry = getService('retry'); + + describe('Basic tests for wizard app ', function () { + before(async () => { + log.debug('navigateToApp wizard'); + await PageObjects.wizard.navigateToCreateWizard(); + }); + + it('should be able to switch data sources', async () => { + const dataSourceValue = await PageObjects.wizard.selectDataSource( + PageObjects.wizard.index.LOGSTASH_NON_TIME_BASED + ); + + expect(dataSourceValue).to.equal(PageObjects.wizard.index.LOGSTASH_NON_TIME_BASED); + // TODO: Switch with a datasource with unique fields to test if it exists + }); + + it('should show visualization when a field is added', async () => { + await PageObjects.wizard.addField('metric', 'Average', 'machine.ram'); + const avgMachineRam = ['13,104,036,080.615', 'Average machine.ram']; + + await retry.try(async function tryingForTime() { + const metricValue = await PageObjects.wizard.getMetric(); + expect(avgMachineRam).to.eql(metricValue); + }); + }); + + it('should clear visualization when field is deleted', async () => { + await PageObjects.wizard.removeField('metric', 0); + + await retry.try(async function tryingForTime() { + const isEmptyWorkspace = await PageObjects.wizard.isEmptyWorkspace(); + expect(isEmptyWorkspace).to.be(true); + }); + }); + }); +} diff --git a/test/functional/apps/wizard/_experimental_vis.ts b/test/functional/apps/wizard/_experimental_vis.ts new file mode 100644 index 000000000000..e36c0254e22a --- /dev/null +++ b/test/functional/apps/wizard/_experimental_vis.ts @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualizations/common/constants'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'wizard']); + const log = getService('log'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + + describe('experimental settings for wizard app ', function () { + it('should show an notification when creating wizard visualization', async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.waitForVisualizationSelectPage(); + + // Try to find the wizard Vis type. + const wizardVisTypeExists = await PageObjects.visualize.hasVisType('wizard'); + expect(wizardVisTypeExists).to.be(true); + + // Create a new visualization + await PageObjects.visualize.clickVisType('wizard'); + + // Check that the experimental banner is there and state that this is experimental + const info = await PageObjects.wizard.getExperimentalInfo(); + expect(await info.getVisibleText()).to.contain('experimental'); + }); + + it('should not be available in the picker when disabled', async () => { + log.debug('navigateToApp visualize'); + await opensearchDashboardsServer.uiSettings.replace({ + [VISUALIZE_ENABLE_LABS_SETTING]: false, + }); + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.waitForVisualizationSelectPage(); + + // Try to find the wizard Vis type. + const wizardVisTypeExists = await PageObjects.visualize.hasVisType('wizard'); + expect(wizardVisTypeExists).to.be(false); + }); + + after(async () => { + // unset the experimental ui setting + await opensearchDashboardsServer.uiSettings.unset(VISUALIZE_ENABLE_LABS_SETTING); + }); + }); +} diff --git a/test/functional/apps/wizard/index.ts b/test/functional/apps/wizard/index.ts new file mode 100644 index 000000000000..24c4eb50c263 --- /dev/null +++ b/test/functional/apps/wizard/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FtrProviderContext } from '../../ftr_provider_context.d'; +import { UI_SETTINGS } from '../../../../src/plugins/data/common'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + + describe('wizard app', function () { + this.tags('ciGroup13'); + + before(async function () { + log.debug('Starting wizard before method'); + await browser.setWindowSize(1280, 800); + await opensearchArchiver.loadIfNeeded('logstash_functional'); + await opensearchArchiver.loadIfNeeded('long_window_logstash'); + await opensearchArchiver.loadIfNeeded('visualize'); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + }); + }); + + after(async () => { + await opensearchArchiver.unload('logstash_functional'); + await opensearchArchiver.unload('long_window_logstash'); + await opensearchArchiver.unload('visualize'); + }); + + loadTestFile(require.resolve('./_base')); + loadTestFile(require.resolve('./_experimental_vis')); + }); +} diff --git a/test/functional/config.js b/test/functional/config.js index 20d3360f0fe5..bb6be73ebd82 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -48,6 +48,7 @@ export default async function ({ readConfigFile }) { require.resolve('./apps/status_page'), require.resolve('./apps/timeline'), require.resolve('./apps/visualize'), + require.resolve('./apps/wizard'), ], pageObjects, services, @@ -91,6 +92,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/visualize', hash: '/', }, + wizard: { + pathname: '/app/wizard', + hash: '/', + }, dashboard: { pathname: '/app/dashboards', hash: '/list', diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 46f8e60e73db..d09445d47026 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -44,6 +44,7 @@ import { TimePickerProvider } from './time_picker'; import { TimelinePageProvider } from './timeline_page'; import { VisualBuilderPageProvider } from './visual_builder_page'; import { VisualizePageProvider } from './visualize_page'; +import { WizardPageProvider } from './wizard_page'; import { VisualizeEditorPageProvider } from './visualize_editor_page'; import { VisualizeChartPageProvider } from './visualize_chart_page'; import { TileMapPageProvider } from './tile_map_page'; @@ -68,6 +69,7 @@ export const pageObjects = { timePicker: TimePickerProvider, visualBuilder: VisualBuilderPageProvider, visualize: VisualizePageProvider, + wizard: WizardPageProvider, visEditor: VisualizeEditorPageProvider, visChart: VisualizeChartPageProvider, tileMap: TileMapPageProvider, diff --git a/test/functional/page_objects/wizard_page.ts b/test/functional/page_objects/wizard_page.ts new file mode 100644 index 000000000000..08ed41df57d5 --- /dev/null +++ b/test/functional/page_objects/wizard_page.ts @@ -0,0 +1,113 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function WizardPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); + const comboBox = getService('comboBox'); + const { common, header } = getPageObjects(['common', 'header']); + + /** + * This page object contains the visualization type selection, the landing page, + * and the open/save dialog functions + */ + class WizardPage { + index = { + LOGSTASH_TIME_BASED: 'logstash-*', + LOGSTASH_NON_TIME_BASED: 'logstash*', + }; + + public async navigateToCreateWizard() { + await common.navigateToApp('wizard'); + await header.waitUntilLoadingHasFinished(); + } + + public async getExperimentalInfo() { + return await testSubjects.find('experimentalVisInfo'); + } + + public async findFieldByName(name: string) { + const fieldSearch = await testSubjects.find('fieldFilterSearchInput'); + await fieldSearch.type(name); + } + + public async getDataSourceSelector() { + const dataSourceDropdown = await testSubjects.find('searchableDropdownValue'); + return await dataSourceDropdown.getVisibleText(); + } + + public async selectDataSource(dataSource: string) { + await testSubjects.click('searchableDropdownValue'); + await find.clickByCssSelector( + `[data-test-subj="searchableDropdownList"] [title="${dataSource}"]` + ); + const dataSourceDropdown = await testSubjects.find('searchableDropdownValue'); + return await dataSourceDropdown.getVisibleText(); + } + + public async addField( + dropBoxId: string, + aggValue: string, + fieldValue: string, + returnToMainPanel = true + ) { + await testSubjects.click(`dropBoxAddField-${dropBoxId} > dropBoxAddBtn`); + await common.sleep(500); + const aggComboBoxElement = await testSubjects.find('defaultEditorAggSelect'); + await comboBox.setElement(aggComboBoxElement, aggValue); + await common.sleep(500); + const fieldComboBoxElement = await testSubjects.find('visDefaultEditorField'); + await comboBox.setElement(fieldComboBoxElement, fieldValue); + await common.sleep(500); + + if (returnToMainPanel) { + await testSubjects.click('panelCloseBtn'); + await common.sleep(500); + } + } + + public async removeField(dropBoxId: string, aggNth: number) { + await testSubjects.click(`dropBoxField-${dropBoxId}-${aggNth} > dropBoxRemoveBtn`); + await common.sleep(500); + } + + // TODO: Fix. Currently it is not able to locate the dropbox location correctly, even if it identifies the element correctly + public async dragDropField(field: string, dropBoxId: string) { + const fieldEle = await testSubjects.find(`field-${field}-showDetails`); + const dropBoxEle = await testSubjects.find(`dropBoxAddField-${dropBoxId}`); + await browser.dragAndDrop({ location: fieldEle }, { location: dropBoxEle }); + } + + public async clearFieldSearchInput() { + const fieldSearch = await testSubjects.find('fieldFilterSearchInput'); + await fieldSearch.clearValue(); + } + + public async getMetric() { + const elements = await find.allByCssSelector( + '[data-test-subj="visualizationLoader"] .mtrVis__container' + ); + const values = await Promise.all( + elements.map(async (element) => { + const text = await element.getVisibleText(); + return text; + }) + ); + return values + .filter((item) => item.length > 0) + .reduce((arr: string[], item) => arr.concat(item.split('\n')), []); + } + + public async isEmptyWorkspace() { + const elements = await find.allByCssSelector('[data-test-subj="emptyWorkspace"]'); + return elements.length === 1; + } + } + + return new WizardPage(); +} diff --git a/test/mocha_decorations.d.ts b/test/mocha_decorations.d.ts index f2e9f2a2dcac..025fd6664c3d 100644 --- a/test/mocha_decorations.d.ts +++ b/test/mocha_decorations.d.ts @@ -42,7 +42,8 @@ type Tags = | 'ciGroup9' | 'ciGroup10' | 'ciGroup11' - | 'ciGroup12'; + | 'ciGroup12' + | 'ciGroup13'; // We need to use the namespace here to match the Mocha definition declare module 'mocha' { diff --git a/test/plugin_functional/test_suites/panel_actions/index.js b/test/plugin_functional/test_suites/panel_actions/index.js index 3fddfd8261e8..a94cfbed1c53 100644 --- a/test/plugin_functional/test_suites/panel_actions/index.js +++ b/test/plugin_functional/test_suites/panel_actions/index.js @@ -30,11 +30,12 @@ import path from 'path'; -export const OPENSEARCH_DASHBOARDS_ARCHIVE_PATH = path.resolve( +const OPENSEARCH_DASHBOARDS_ARCHIVE_PATH = path.resolve( __dirname, '../../../functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards' ); -export const DATA_ARCHIVE_PATH = path.resolve( + +const DATA_ARCHIVE_PATH = path.resolve( __dirname, '../../../functional/fixtures/opensearch_archiver/dashboard/current/data' );