From d4094c17bec4692e1ee942adeaa174e2404e2dfc Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 2 Dec 2024 20:57:29 +0100 Subject: [PATCH] [scout] migrate more Discover tests (#201842) ## Summary This PR migrates the following FTR tests to `@kbn/scout`: `x-pack/test/functional/apps/discover/error_handling.ts` => `x-pack/plugins/discover_enhanced/ui_tests/tests/error_handling.spec.ts` `x-pack/test/functional/apps/discover/saved_search_embeddable.ts` => `x-pack/plugins/discover_enhanced/ui_tests/tests/saved_search_embeddable.spec.ts` `x-pack/test/functional/apps/discover/saved_searches.ts` => `x-pack/plugins/discover_enhanced/ui_tests/tests/saved_searches.spec.ts` `x-pack/test/functional/apps/discover/value_suggestions.ts` 2nd describe block => `x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_use_time_range_disabled.spec.ts` Some other changes to mention: **packages/kbn-test-subj-selector**: - support of `^foo` syntax similar to `CSS [attribute^=value] Selector` **packages/kbn-scout**: - new worker fixture `uiSettings` to wrap Advanced Settings set/unset capability - extend `ScoutPage` fixture with `typeWithDelay` method required for many Kibana input fields - extend `PageObjects` fixture with `DashboardApp` & `FilterBar`, also extending existing ones. How to test: ```bash // ESS node scripts/scout_start_servers.js --stateful npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts --grep @ess // Serverless node scripts/scout_start_servers.js --serverless=es npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts --grep @svlSearch ``` --------- Co-authored-by: Robert Oskamp --- packages/kbn-scout/index.ts | 3 + .../src/playwright/fixtures/index.ts | 1 + .../src/playwright/fixtures/test/page.ts | 34 ++++- .../playwright/fixtures/types/test_scope.ts | 47 +++++++ .../playwright/fixtures/types/worker_scope.ts | 10 +- .../src/playwright/fixtures/worker/core.ts | 84 ++++++++++++ .../src/playwright/fixtures/worker/index.ts | 78 +---------- .../playwright/fixtures/worker/ui_settings.ts | 37 +++++ packages/kbn-scout/src/playwright/index.ts | 7 +- .../playwright/page_objects/dashboard_app.ts | 95 +++++++++++++ .../playwright/page_objects/date_picker.ts | 42 +++++- .../playwright/page_objects/discover_app.ts | 31 +++++ .../src/playwright/page_objects/fiter_bar.ts | 70 ++++++++++ .../src/playwright/page_objects/index.ts | 6 + .../kbn-scout/src/playwright/utils/index.ts | 10 ++ packages/kbn-scout/src/types/index.ts | 1 + packages/kbn-scout/src/types/services.d.ts | 13 ++ .../test_subj_selector.ts | 17 ++- .../discover_enhanced/ui_tests/README.md | 5 +- .../ui_tests/fixtures/assertion_messages.ts | 10 ++ .../ui_tests/fixtures/constants.ts | 44 ++++++ .../ui_tests/fixtures/index.ts | 5 +- .../ui_tests/tests/error_handling.spec.ts | 38 ++++++ .../tests/saved_search_embeddable.spec.ts | 102 ++++++++++++++ .../ui_tests/tests/saved_searches.spec.ts | 129 ++++++++++++++++++ .../ui_tests/tests/value_suggestions.spec.ts | 82 +++++------ .../value_suggestions_non_time_based.spec.ts | 66 ++++----- ...uggestions_use_time_range_disabled.spec.ts | 63 +++++++++ 28 files changed, 969 insertions(+), 161 deletions(-) create mode 100644 packages/kbn-scout/src/playwright/fixtures/worker/core.ts create mode 100644 packages/kbn-scout/src/playwright/fixtures/worker/ui_settings.ts create mode 100644 packages/kbn-scout/src/playwright/page_objects/dashboard_app.ts create mode 100644 packages/kbn-scout/src/playwright/page_objects/fiter_bar.ts create mode 100644 packages/kbn-scout/src/types/services.d.ts create mode 100644 x-pack/plugins/discover_enhanced/ui_tests/fixtures/assertion_messages.ts create mode 100644 x-pack/plugins/discover_enhanced/ui_tests/fixtures/constants.ts create mode 100644 x-pack/plugins/discover_enhanced/ui_tests/tests/error_handling.spec.ts create mode 100644 x-pack/plugins/discover_enhanced/ui_tests/tests/saved_search_embeddable.spec.ts create mode 100644 x-pack/plugins/discover_enhanced/ui_tests/tests/saved_searches.spec.ts create mode 100644 x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_use_time_range_disabled.spec.ts diff --git a/packages/kbn-scout/index.ts b/packages/kbn-scout/index.ts index 2cbf98d96a8e0..34a906fcf755d 100644 --- a/packages/kbn-scout/index.ts +++ b/packages/kbn-scout/index.ts @@ -16,4 +16,7 @@ export type { PageObjects, ScoutTestFixtures, ScoutWorkerFixtures, + EsArchiverFixture, } from './src/playwright'; + +export type { Client, KbnClient, KibanaUrl, SamlSessionManager, ToolingLog } from './src/types'; diff --git a/packages/kbn-scout/src/playwright/fixtures/index.ts b/packages/kbn-scout/src/playwright/fixtures/index.ts index 348b581005994..a17a592b5ada0 100644 --- a/packages/kbn-scout/src/playwright/fixtures/index.ts +++ b/packages/kbn-scout/src/playwright/fixtures/index.ts @@ -15,6 +15,7 @@ import { scoutTestFixtures } from './test'; export const scoutCoreFixtures = mergeTests(scoutWorkerFixtures, scoutTestFixtures); export type { + EsArchiverFixture, ScoutTestFixtures, ScoutWorkerFixtures, ScoutPage, diff --git a/packages/kbn-scout/src/playwright/fixtures/test/page.ts b/packages/kbn-scout/src/playwright/fixtures/test/page.ts index ffc309d37cbad..b41b8a92f2701 100644 --- a/packages/kbn-scout/src/playwright/fixtures/test/page.ts +++ b/packages/kbn-scout/src/playwright/fixtures/test/page.ts @@ -15,7 +15,7 @@ import { ScoutPage, KibanaUrl } from '../types'; * Instead of defining each method individually, we use a list of method names and loop through them, creating methods dynamically. * All methods must have 'selector: string' as the first argument */ -function extendPageWithTestSubject(page: Page) { +function extendPageWithTestSubject(page: Page): ScoutPage['testSubj'] { const methods: Array = [ 'check', 'click', @@ -28,10 +28,15 @@ function extendPageWithTestSubject(page: Page) { 'innerText', 'isChecked', 'isHidden', + 'isVisible', 'locator', + 'waitForSelector', ]; - const extendedMethods: Partial> = {}; + const extendedMethods: Partial> & { + typeWithDelay?: ScoutPage['testSubj']['typeWithDelay']; + clearInput?: ScoutPage['testSubj']['clearInput']; + } = {}; for (const method of methods) { extendedMethods[method] = (...args: any[]) => { @@ -41,7 +46,27 @@ function extendPageWithTestSubject(page: Page) { }; } - return extendedMethods as Record; + // custom method to types text into an input field character by character with a delay + extendedMethods.typeWithDelay = async ( + selector: string, + text: string, + options?: { delay: number } + ) => { + const { delay = 25 } = options || {}; + const testSubjSelector = subj(selector); + await page.locator(testSubjSelector).click(); + for (const char of text) { + await page.keyboard.insertText(char); + await page.waitForTimeout(delay); + } + }; + // custom method to clear an input field + extendedMethods.clearInput = async (selector: string) => { + const testSubjSelector = subj(selector); + await page.locator(testSubjSelector).fill(''); + }; + + return extendedMethods as ScoutPage['testSubj']; } /** @@ -78,6 +103,9 @@ export const scoutPageFixture = base.extend<{ page: ScoutPage; kbnUrl: KibanaUrl // Method to navigate to specific Kibana apps page.gotoApp = (appName: string) => page.goto(kbnUrl.app(appName)); + page.waitForLoadingIndicatorHidden = () => + page.testSubj.waitForSelector('globalLoadingIndicator-hidden', { state: 'attached' }); + await use(page); }, }); diff --git a/packages/kbn-scout/src/playwright/fixtures/types/test_scope.ts b/packages/kbn-scout/src/playwright/fixtures/types/test_scope.ts index 2808381f0f6be..4ff7fc18cd6f2 100644 --- a/packages/kbn-scout/src/playwright/fixtures/types/test_scope.ts +++ b/packages/kbn-scout/src/playwright/fixtures/types/test_scope.ts @@ -22,8 +22,30 @@ export interface LoginFixture { loginAsPrivilegedUser: () => Promise; } +/** + * Extends the Playwright 'Page' interface with methods specific to Kibana. + * Reasons to use 'ReturnType' instead of Explicit Typings: + * - DRY Principle: automatically stays in sync with the Playwright API, reducing maintenance overhead. + * - Future-Proofing: If Playwright changes the return type of methods, these types will update accordingly. + * Recommendation: define Explicit Types as return types only if methods (e.g. 'typeWithDelay') + * have any additional logic or Kibana-specific behavior. + */ export type ScoutPage = Page & { + /** + * Navigates to the specified Kibana application. + * @param appName - The name of the Kibana app (e.g., 'discover', 'dashboard'). + * @param options - Additional navigation options, passed directly to Playwright's `goto` method. + * @returns A Promise resolving to a Playwright `Response` or `null`. + */ gotoApp: (appName: string, options?: Parameters[1]) => ReturnType; + /** + * Waits for the Kibana loading spinner indicator to disappear. + * @returns A Promise resolving when the indicator is hidden. + */ + waitForLoadingIndicatorHidden: () => ReturnType; + /** + * Simplified API to interact with elements using Kibana's 'data-test-subj' attribute. + */ testSubj: { check: (selector: string, options?: Parameters[1]) => ReturnType; click: (selector: string, options?: Parameters[1]) => ReturnType; @@ -59,9 +81,34 @@ export type ScoutPage = Page & { selector: string, options?: Parameters[1] ) => ReturnType; + isVisible: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; locator: ( selector: string, options?: Parameters[1] ) => ReturnType; + waitForSelector: ( + selector: string, + options?: Parameters[1] + ) => ReturnType; + // custom methods + /** + * Types text into an input field character by character with a specified delay between each character. + * + * @param selector - The selector for the input element (supports 'data-test-subj' attributes). + * @param text - The text to type into the input field. + * @param options - Optional configuration object. + * @param options.delay - The delay in milliseconds between typing each character (default: 25ms). + * @returns A Promise that resolves once the text has been typed. + */ + typeWithDelay: (selector: string, text: string, options?: { delay: number }) => Promise; + /** + * Clears the input field by filling it with an empty string. + * @param selector The selector for the input element (supports 'data-test-subj' attributes). + * @returns A Promise that resolves once the text has been cleared. + */ + clearInput: (selector: string) => Promise; }; }; diff --git a/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts b/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts index c9424dc0f5970..20908a566abb2 100644 --- a/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts +++ b/packages/kbn-scout/src/playwright/fixtures/types/worker_scope.ts @@ -12,23 +12,31 @@ import type { ToolingLog } from '@kbn/tooling-log'; import type { Client } from '@elastic/elasticsearch'; import { LoadActionPerfOptions } from '@kbn/es-archiver'; import { IndexStats } from '@kbn/es-archiver/src/lib/stats'; +import type { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings'; import { ScoutServerConfig } from '../../../types'; import { KibanaUrl } from '../../../common/services/kibana_url'; -interface EsArchiverFixture { +export interface EsArchiverFixture { loadIfNeeded: ( name: string, performance?: LoadActionPerfOptions | undefined ) => Promise>; } +export interface UiSettingsFixture { + set: (values: UiSettingValues) => Promise; + unset: (...values: string[]) => Promise; + setDefaultTime: ({ from, to }: { from: string; to: string }) => Promise; +} + export interface ScoutWorkerFixtures { log: ToolingLog; config: ScoutServerConfig; kbnUrl: KibanaUrl; esClient: Client; kbnClient: KbnClient; + uiSettings: UiSettingsFixture; esArchiver: EsArchiverFixture; samlAuth: SamlSessionManager; } diff --git a/packages/kbn-scout/src/playwright/fixtures/worker/core.ts b/packages/kbn-scout/src/playwright/fixtures/worker/core.ts new file mode 100644 index 0000000000000..359a779b05fc7 --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/worker/core.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { test as base } from '@playwright/test'; + +import { LoadActionPerfOptions } from '@kbn/es-archiver'; +import { + createKbnUrl, + createEsArchiver, + createEsClient, + createKbnClient, + createLogger, + createSamlSessionManager, + createScoutConfig, +} from '../../../common/services'; +import { ScoutWorkerFixtures } from '../types/worker_scope'; +import { ScoutTestOptions } from '../../types'; + +export const coreWorkerFixtures = base.extend<{}, ScoutWorkerFixtures>({ + log: [ + ({}, use) => { + use(createLogger()); + }, + { scope: 'worker' }, + ], + + config: [ + ({ log }, use, testInfo) => { + const configName = 'local'; + const projectUse = testInfo.project.use as ScoutTestOptions; + const serversConfigDir = projectUse.serversConfigDir; + const configInstance = createScoutConfig(serversConfigDir, configName, log); + + use(configInstance); + }, + { scope: 'worker' }, + ], + + kbnUrl: [ + ({ config, log }, use) => { + use(createKbnUrl(config, log)); + }, + { scope: 'worker' }, + ], + + esClient: [ + ({ config, log }, use) => { + use(createEsClient(config, log)); + }, + { scope: 'worker' }, + ], + + kbnClient: [ + ({ log, config }, use) => { + use(createKbnClient(config, log)); + }, + { scope: 'worker' }, + ], + + esArchiver: [ + ({ log, esClient, kbnClient }, use) => { + const esArchiverInstance = createEsArchiver(esClient, kbnClient, log); + // to speedup test execution we only allow to ingest the data indexes and only if index doesn't exist + const loadIfNeeded = async (name: string, performance?: LoadActionPerfOptions | undefined) => + esArchiverInstance!.loadIfNeeded(name, performance); + + use({ loadIfNeeded }); + }, + { scope: 'worker' }, + ], + + samlAuth: [ + ({ log, config }, use) => { + use(createSamlSessionManager(config, log)); + }, + { scope: 'worker' }, + ], +}); diff --git a/packages/kbn-scout/src/playwright/fixtures/worker/index.ts b/packages/kbn-scout/src/playwright/fixtures/worker/index.ts index c61d9755c44db..cfc697bf03613 100644 --- a/packages/kbn-scout/src/playwright/fixtures/worker/index.ts +++ b/packages/kbn-scout/src/playwright/fixtures/worker/index.ts @@ -7,78 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { test as base } from '@playwright/test'; +import { mergeTests } from 'playwright/test'; +import { uiSettingsFixture } from './ui_settings'; +import { coreWorkerFixtures } from './core'; -import { LoadActionPerfOptions } from '@kbn/es-archiver'; -import { - createKbnUrl, - createEsArchiver, - createEsClient, - createKbnClient, - createLogger, - createSamlSessionManager, - createScoutConfig, -} from '../../../common/services'; -import { ScoutWorkerFixtures } from '../types/worker_scope'; -import { ScoutTestOptions } from '../../types'; - -export const scoutWorkerFixtures = base.extend<{}, ScoutWorkerFixtures>({ - log: [ - ({}, use) => { - use(createLogger()); - }, - { scope: 'worker' }, - ], - - config: [ - ({ log }, use, testInfo) => { - const configName = 'local'; - const projectUse = testInfo.project.use as ScoutTestOptions; - const serversConfigDir = projectUse.serversConfigDir; - const configInstance = createScoutConfig(serversConfigDir, configName, log); - - use(configInstance); - }, - { scope: 'worker' }, - ], - - kbnUrl: [ - ({ config, log }, use) => { - use(createKbnUrl(config, log)); - }, - { scope: 'worker' }, - ], - - esClient: [ - ({ config, log }, use) => { - use(createEsClient(config, log)); - }, - { scope: 'worker' }, - ], - - kbnClient: [ - ({ log, config }, use) => { - use(createKbnClient(config, log)); - }, - { scope: 'worker' }, - ], - - esArchiver: [ - ({ log, esClient, kbnClient }, use) => { - const esArchiverInstance = createEsArchiver(esClient, kbnClient, log); - // to speedup test execution we only allow to ingest the data indexes and only if index doesn't exist - const loadIfNeeded = async (name: string, performance?: LoadActionPerfOptions | undefined) => - esArchiverInstance!.loadIfNeeded(name, performance); - - use({ loadIfNeeded }); - }, - { scope: 'worker' }, - ], - - samlAuth: [ - ({ log, config }, use) => { - use(createSamlSessionManager(config, log)); - }, - { scope: 'worker' }, - ], -}); +export const scoutWorkerFixtures = mergeTests(coreWorkerFixtures, uiSettingsFixture); diff --git a/packages/kbn-scout/src/playwright/fixtures/worker/ui_settings.ts b/packages/kbn-scout/src/playwright/fixtures/worker/ui_settings.ts new file mode 100644 index 0000000000000..989f7ce436616 --- /dev/null +++ b/packages/kbn-scout/src/playwright/fixtures/worker/ui_settings.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { test as base } from '@playwright/test'; +import { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings'; +import { ScoutWorkerFixtures } from '../types'; +import { isValidUTCDate, formatTime } from '../../utils'; + +export const uiSettingsFixture = base.extend<{}, ScoutWorkerFixtures>({ + uiSettings: [ + ({ kbnClient }, use) => { + const kbnClientUiSettings = { + set: async (values: UiSettingValues) => kbnClient.uiSettings.update(values), + + unset: async (...keys: string[]) => + Promise.all(keys.map((key) => kbnClient.uiSettings.unset(key))), + + setDefaultTime: async ({ from, to }: { from: string; to: string }) => { + const utcFrom = isValidUTCDate(from) ? from : formatTime(from); + const untcTo = isValidUTCDate(to) ? to : formatTime(to); + await kbnClient.uiSettings.update({ + 'timepicker:timeDefaults': `{ "from": "${utcFrom}", "to": "${untcTo}"}`, + }); + }, + }; + + use(kbnClientUiSettings); + }, + { scope: 'worker' }, + ], +}); diff --git a/packages/kbn-scout/src/playwright/index.ts b/packages/kbn-scout/src/playwright/index.ts index 66c80f0068f06..5294274c41bc5 100644 --- a/packages/kbn-scout/src/playwright/index.ts +++ b/packages/kbn-scout/src/playwright/index.ts @@ -19,4 +19,9 @@ export { expect } from './expect'; export type { ScoutPlaywrightOptions, ScoutTestOptions } from './types'; export type { PageObjects } from './page_objects'; -export type { ScoutTestFixtures, ScoutWorkerFixtures, ScoutPage } from './fixtures'; +export type { + ScoutTestFixtures, + ScoutWorkerFixtures, + ScoutPage, + EsArchiverFixture, +} from './fixtures'; diff --git a/packages/kbn-scout/src/playwright/page_objects/dashboard_app.ts b/packages/kbn-scout/src/playwright/page_objects/dashboard_app.ts new file mode 100644 index 0000000000000..367167c0304f2 --- /dev/null +++ b/packages/kbn-scout/src/playwright/page_objects/dashboard_app.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutPage } from '../fixtures/types'; + +type CommonlyUsedTimeRange = + | 'Today' + | 'Last_15 minutes' + | 'Last_1 hour' + | 'Last_24 hours' + | 'Last_30 days' + | 'Last_90 days' + | 'Last_1 year'; + +export class DashboardApp { + constructor(private readonly page: ScoutPage) {} + + async goto() { + await this.page.gotoApp('dashboards'); + } + + async openNewDashboard() { + await this.page.testSubj.click('newItemButton'); + await this.page.testSubj.waitForSelector('emptyDashboardWidget', { state: 'visible' }); + } + + async saveDashboard(name: string) { + await this.page.testSubj.click('dashboardInteractiveSaveMenuItem'); + await this.page.testSubj.fill('savedObjectTitle', name); + await this.page.testSubj.click('confirmSaveSavedObjectButton'); + await this.page.testSubj.waitForSelector('confirmSaveSavedObjectButton', { state: 'hidden' }); + } + + async addPanelFromLibrary(...names: string[]) { + await this.page.testSubj.click('dashboardAddFromLibraryButton'); + for (let i = 0; i < names.length; i++) { + // clear search input after the first panel is added + if (i > 0) { + await this.page.testSubj.clearInput('savedObjectFinderSearchInput'); + } + await this.page.testSubj.typeWithDelay('savedObjectFinderSearchInput', names[i]); + await this.page.testSubj.click(`savedObjectTitle${names[i].replace(/ /g, '-')}`); + // wait for the panel to be added + await this.page.testSubj.waitForSelector( + `embeddablePanelHeading-${names[i].replace(/[- ]/g, '')}`, + { + state: 'visible', + } + ); + } + // close the flyout + await this.page.testSubj.click('euiFlyoutCloseButton'); + await this.page.testSubj.waitForSelector('euiFlyoutCloseButton', { state: 'hidden' }); + } + + async customizePanel(options: { + name: string; + customTimeRageCommonlyUsed?: { + value: CommonlyUsedTimeRange; + }; + }) { + await this.page.testSubj.hover(`embeddablePanelHeading-${options.name.replace(/ /g, '')}`); + await this.page.testSubj.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); + if (options.customTimeRageCommonlyUsed) { + await this.page.testSubj.click('customizePanelShowCustomTimeRange'); + await this.page.testSubj.click( + 'customizePanelTimeRangeDatePicker > superDatePickerToggleQuickMenuButton' + ); + await this.page.testSubj.click( + `superDatePickerCommonlyUsed_${options.customTimeRageCommonlyUsed.value}` + ); + } + + await this.page.testSubj.click('saveCustomizePanelButton'); + await this.page.testSubj.waitForSelector('saveCustomizePanelButton', { state: 'hidden' }); + } + + async removePanel(name: string | 'embeddableError') { + const panelHeaderTestSubj = + name === 'embeddableError' ? name : `embeddablePanelHeading-${name.replace(/ /g, '')}`; + await this.page.testSubj.locator(panelHeaderTestSubj).scrollIntoViewIfNeeded(); + await this.page.testSubj.locator(panelHeaderTestSubj).hover(); + await this.page.testSubj.click('embeddablePanelToggleMenuIcon'); + await this.page.testSubj.click('embeddablePanelAction-deletePanel'); + await this.page.testSubj.waitForSelector(panelHeaderTestSubj, { + state: 'hidden', + }); + } +} diff --git a/packages/kbn-scout/src/playwright/page_objects/date_picker.ts b/packages/kbn-scout/src/playwright/page_objects/date_picker.ts index 08b724a956a3d..d5d26063fb0b0 100644 --- a/packages/kbn-scout/src/playwright/page_objects/date_picker.ts +++ b/packages/kbn-scout/src/playwright/page_objects/date_picker.ts @@ -13,8 +13,36 @@ import { expect } from '..'; export class DatePicker { constructor(private readonly page: ScoutPage) {} + private async showStartEndTimes() { + // This first await makes sure the superDatePicker has loaded before we check for the ShowDatesButton + await this.page.testSubj.waitForSelector('superDatePickerToggleQuickMenuButton', { + timeout: 10000, + }); + const isShowDateBtnVisible = await this.page.testSubj.isVisible( + 'superDatePickerShowDatesButton', + { + timeout: 5000, + } + ); + + if (isShowDateBtnVisible) { + await this.page.testSubj.click('superDatePickerShowDatesButton'); + } + + const isStartDatePopoverBtnVisible = await this.page.testSubj.isVisible( + 'superDatePickerstartDatePopoverButton', + { + timeout: 5000, + } + ); + + if (isStartDatePopoverBtnVisible) { + await this.page.testSubj.click('superDatePickerstartDatePopoverButton'); + } + } + async setAbsoluteRange({ from, to }: { from: string; to: string }) { - await this.page.testSubj.click('superDatePickerShowDatesButton'); + await this.showStartEndTimes(); // we start with end date await this.page.testSubj.click('superDatePickerendDatePopoverButton'); await this.page.testSubj.click('superDatePickerAbsoluteTab'); @@ -32,10 +60,14 @@ export class DatePicker { await this.page.testSubj.click('parseAbsoluteDateFormat'); await this.page.keyboard.press('Escape'); - await expect(this.page.testSubj.locator('superDatePickerstartDatePopoverButton')).toHaveText( - from - ); - await expect(this.page.testSubj.locator('superDatePickerendDatePopoverButton')).toHaveText(to); + await expect( + this.page.testSubj.locator('superDatePickerstartDatePopoverButton'), + `Date picker 'start date' should be set correctly` + ).toHaveText(from); + await expect( + this.page.testSubj.locator('superDatePickerendDatePopoverButton'), + `Date picker 'end date' should be set correctly` + ).toHaveText(to); await this.page.testSubj.click('querySubmitButton'); } } diff --git a/packages/kbn-scout/src/playwright/page_objects/discover_app.ts b/packages/kbn-scout/src/playwright/page_objects/discover_app.ts index e4abbf252ae31..bb82984359c79 100644 --- a/packages/kbn-scout/src/playwright/page_objects/discover_app.ts +++ b/packages/kbn-scout/src/playwright/page_objects/discover_app.ts @@ -15,4 +15,35 @@ export class DiscoverApp { async goto() { await this.page.gotoApp('discover'); } + + async selectDataView(name: string) { + const currentValue = await this.page.testSubj.innerText('*dataView-switch-link'); + if (currentValue === name) { + return; + } + await this.page.testSubj.click('*dataView-switch-link'); + await this.page.testSubj.waitForSelector('indexPattern-switcher'); + await this.page.testSubj.typeWithDelay('indexPattern-switcher--input', name); + await this.page.testSubj.locator('indexPattern-switcher').locator(`[title="${name}"]`).click(); + await this.page.testSubj.waitForSelector('indexPattern-switcher', { state: 'hidden' }); + await this.page.waitForLoadingIndicatorHidden(); + } + + async clickNewSearch() { + await this.page.testSubj.hover('discoverNewButton'); + await this.page.testSubj.click('discoverNewButton'); + await this.page.testSubj.hover('unifiedFieldListSidebar__toggle-collapse'); // cancel tooltips + } + + async saveSearch(name: string) { + await this.page.testSubj.click('discoverSaveButton'); + await this.page.testSubj.fill('savedObjectTitle', name); + await this.page.testSubj.click('confirmSaveSavedObjectButton'); + await this.page.testSubj.waitForSelector('savedObjectSaveModal', { state: 'hidden' }); + await this.page.waitForLoadingIndicatorHidden(); + } + + async waitForHistogramRendered() { + await this.page.testSubj.waitForSelector('unifiedHistogramRendered'); + } } diff --git a/packages/kbn-scout/src/playwright/page_objects/fiter_bar.ts b/packages/kbn-scout/src/playwright/page_objects/fiter_bar.ts new file mode 100644 index 0000000000000..6026e16b9edb3 --- /dev/null +++ b/packages/kbn-scout/src/playwright/page_objects/fiter_bar.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ScoutPage } from '../fixtures/types'; +import { expect } from '..'; + +interface FilterCreationOptions { + field: string; + operator: 'is' | 'is not' | 'is one of' | 'is not one of' | 'exists' | 'does not exist'; + value: string; +} + +interface FilterStateOptions { + field: string; + value: string; + enabled?: boolean; + pinned?: boolean; + negated?: boolean; +} + +export class FilterBar { + constructor(private readonly page: ScoutPage) {} + + async addFilter(options: FilterCreationOptions) { + await this.page.testSubj.click('addFilter'); + await this.page.testSubj.waitForSelector('addFilterPopover'); + // set field name + await this.page.testSubj.typeWithDelay( + 'filterFieldSuggestionList > comboBoxSearchInput', + options.field + ); + await this.page.click(`.euiFilterSelectItem[title="${options.field}"]`); + // set operator + await this.page.testSubj.typeWithDelay( + 'filterOperatorList > comboBoxSearchInput', + options.operator + ); + await this.page.click(`.euiFilterSelectItem[title="${options.operator}"]`); + // set value + await this.page.testSubj.locator('filterParams').locator('input').fill(options.value); + // save filter + await this.page.testSubj.click('saveFilter'); + + await expect( + this.page.testSubj.locator('^filter-badge'), + 'New filter badge should be displayed' + ).toBeVisible(); + } + + async hasFilter(options: FilterStateOptions) { + const testSubjLocator = [ + '~filter', + options.enabled !== undefined && `~filter-${options.enabled ? 'enabled' : 'disabled'}`, + options.field && `~filter-key-${options.field}`, + options.value && `~filter-value-${options.value}`, + options.pinned !== undefined && `~filter-${options.pinned ? 'pinned' : 'unpinned'}`, + options.negated !== undefined && (options.negated ? '~filter-negated' : ''), + ] + .filter(Boolean) + .join(' & '); + + return this.page.testSubj.isVisible(testSubjLocator, { strict: true }); + } +} diff --git a/packages/kbn-scout/src/playwright/page_objects/index.ts b/packages/kbn-scout/src/playwright/page_objects/index.ts index fb90dfea38ff8..19bc81da669a4 100644 --- a/packages/kbn-scout/src/playwright/page_objects/index.ts +++ b/packages/kbn-scout/src/playwright/page_objects/index.ts @@ -8,13 +8,17 @@ */ import { ScoutPage } from '../fixtures/types'; +import { DashboardApp } from './dashboard_app'; import { DatePicker } from './date_picker'; import { DiscoverApp } from './discover_app'; +import { FilterBar } from './fiter_bar'; import { createLazyPageObject } from './utils'; export interface PageObjects { datePicker: DatePicker; discover: DiscoverApp; + dashboard: DashboardApp; + filterBar: FilterBar; } /** @@ -26,7 +30,9 @@ export interface PageObjects { export function createCorePageObjects(page: ScoutPage): PageObjects { return { datePicker: createLazyPageObject(DatePicker, page), + dashboard: createLazyPageObject(DashboardApp, page), discover: createLazyPageObject(DiscoverApp, page), + filterBar: createLazyPageObject(FilterBar, page), // Add new page objects here }; } diff --git a/packages/kbn-scout/src/playwright/utils/index.ts b/packages/kbn-scout/src/playwright/utils/index.ts index 6100cffc2f2c8..4b6fcafcbcfa8 100644 --- a/packages/kbn-scout/src/playwright/utils/index.ts +++ b/packages/kbn-scout/src/playwright/utils/index.ts @@ -7,4 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import moment from 'moment'; + export const serviceLoadedMsg = (name: string) => `scout service loaded: ${name}`; + +export const isValidUTCDate = (date: string): boolean => { + return !isNaN(Date.parse(date)) && new Date(date).toISOString() === date; +}; + +export function formatTime(date: string, fmt: string = 'MMM D, YYYY @ HH:mm:ss.SSS') { + return moment.utc(date, fmt).format(); +} diff --git a/packages/kbn-scout/src/types/index.ts b/packages/kbn-scout/src/types/index.ts index 811b63fb1aac3..024a00ec29e45 100644 --- a/packages/kbn-scout/src/types/index.ts +++ b/packages/kbn-scout/src/types/index.ts @@ -10,3 +10,4 @@ export * from './config'; export * from './cli'; export * from './servers'; +export * from './services'; diff --git a/packages/kbn-scout/src/types/services.d.ts b/packages/kbn-scout/src/types/services.d.ts new file mode 100644 index 0000000000000..2b653354043d9 --- /dev/null +++ b/packages/kbn-scout/src/types/services.d.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export type { KbnClient, SamlSessionManager } from '@kbn/test'; +export type { Client } from '@elastic/elasticsearch'; +export type { KibanaUrl } from '../common/services'; +export type { ToolingLog } from '@kbn/tooling-log'; diff --git a/packages/kbn-test-subj-selector/test_subj_selector.ts b/packages/kbn-test-subj-selector/test_subj_selector.ts index 6651873cab434..66d4c6c400668 100644 --- a/packages/kbn-test-subj-selector/test_subj_selector.ts +++ b/packages/kbn-test-subj-selector/test_subj_selector.ts @@ -22,6 +22,8 @@ function termToCssSelector(term: string) { return '[data-test-subj~="' + term.substring(1).replace(/\s/g, '') + '"]'; } else if (term.startsWith('*')) { return '[data-test-subj*="' + term.substring(1).replace(/\s/g, '') + '"]'; + } else if (term.startsWith('^')) { + return '[data-test-subj^="' + term.substring(1).replace(/\s/g, '') + '"]'; } else { return '[data-test-subj="' + term + '"]'; } @@ -40,25 +42,30 @@ function termToCssSelector(term: string) { * - prefixing a value with `*` will allow matching a `data-test-subj` attribute containing at least one occurrence of value within the string. * - example: `*foo` * - css equivalent: `[data-test-subj*="foo"]` - * - DOM match example:
data-test-subj="bar-foo"
+ * - DOM match example:
+ * + * - prefixing a value with `^` will allow matching a `data-test-subj` attribute beginning with the specified value. + * - example: `^foo` + * - css equivalent: `[data-test-subj^="foo"]` + * - DOM match example:
* * - prefixing a value with `~` will allow matching a `data-test-subj` attribute represented as a whitespace-separated list of words, one of which is exactly value * - example: `~foo` * - css equivalent: `[data-test-subj~="foo"]` - * - DOM match example:
data-test-subj="foo bar"
+ * - DOM match example:
* * - the `>` character is used between two values to indicate that the value on the right must match an element inside an element matched by the value on the left * - example: `foo > bar` * - css equivalent: `[data-test-subj=foo] [data-test-subj=bar]` * - DOM match example: - *
data-test-subj="foo" - *
data-test-subj="bar"
+ *
+ *
*
* * - the `&` character is used between two values to indicate that the value on both sides must both match the element * - example: `foo & bar` * - css equivalent: `[data-test-subj=foo][data-test-subj=bar]` - * - DOM match example:
data-test-subj="foo bar"
+ * - DOM match example:
*/ export function subj(selector: string) { return selectorToTerms(selector) diff --git a/x-pack/plugins/discover_enhanced/ui_tests/README.md b/x-pack/plugins/discover_enhanced/ui_tests/README.md index 8320e9464d9ca..e6c5943e1533f 100644 --- a/x-pack/plugins/discover_enhanced/ui_tests/README.md +++ b/x-pack/plugins/discover_enhanced/ui_tests/README.md @@ -11,7 +11,10 @@ node scripts/scout_start_servers.js --serverless=es Then you can run the tests multiple times in another terminal with: ```bash -npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts +// ESS +npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts --grep @ess +// Serverless +npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts --grep @svlSearch // @svlOblt, @svlSecurity ``` Test results are available in `x-pack/plugins/discover_enhanced/ui_tests/output` diff --git a/x-pack/plugins/discover_enhanced/ui_tests/fixtures/assertion_messages.ts b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/assertion_messages.ts new file mode 100644 index 0000000000000..6ccbe4df990cb --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/assertion_messages.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const QUERY_BAR_VALIDATION = { + SUGGESTIONS_COUNT: 'The query bar suggestions count should be', +}; diff --git a/x-pack/plugins/discover_enhanced/ui_tests/fixtures/constants.ts b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/constants.ts new file mode 100644 index 0000000000000..3ac2c34586f4d --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/constants.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const LOGSTASH_DEFAULT_START_TIME = '2015-09-19T06:31:44.000Z'; +export const LOGSTASH_DEFAULT_END_TIME = '2015-09-23T18:31:44.000Z'; + +export const DATA_VIEW_ID = { + ECOMMERCE: '5193f870-d861-11e9-a311-0fa548c5f953', + LOGSTASH: 'logstash-*', +}; + +export const DATA_VIEW = { + ECOMMERCE: 'ecommerce', + LOGSTASH: 'logstash-*', +}; + +export const LOGSTASH_OUT_OF_RANGE_DATES = { + from: 'Mar 1, 2020 @ 00:00:00.000', + to: 'Nov 1, 2020 @ 00:00:00.000', +}; + +export const LOGSTASH_IN_RANGE_DATES = { + from: 'Sep 19, 2015 @ 06:31:44.000', + to: 'Sep 23, 2015 @ 18:31:44.000', +}; + +export const ES_ARCHIVES = { + LOGSTASH: 'x-pack/test/functional/es_archives/logstash_functional', + NO_TIME_FIELD: 'test/functional/fixtures/es_archiver/index_pattern_without_timefield', + ECOMMERCE: 'x-pack/test/functional/es_archives/reporting/ecommerce', +}; + +export const KBN_ARCHIVES = { + INVALID_SCRIPTED_FIELD: 'test/functional/fixtures/kbn_archiver/invalid_scripted_field', + NO_TIME_FIELD: 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield', + DASHBOARD_DRILLDOWNS: + 'x-pack/test/functional/fixtures/kbn_archiver/dashboard_drilldowns/drilldowns', + DISCOVER: 'test/functional/fixtures/kbn_archiver/discover', + ECOMMERCE: 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json', +}; diff --git a/x-pack/plugins/discover_enhanced/ui_tests/fixtures/index.ts b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/index.ts index 38d4905f82e6f..4cff802b3b89f 100644 --- a/x-pack/plugins/discover_enhanced/ui_tests/fixtures/index.ts +++ b/x-pack/plugins/discover_enhanced/ui_tests/fixtures/index.ts @@ -14,7 +14,7 @@ import { } from '@kbn/scout'; import { DemoPage } from './page_objects'; -interface ExtendedScoutTestFixtures extends ScoutTestFixtures { +export interface ExtendedScoutTestFixtures extends ScoutTestFixtures { pageObjects: PageObjects & { demo: DemoPage; }; @@ -30,3 +30,6 @@ export const test = base.extend( await use(extendedPageObjects); }, }); + +export * as testData from './constants'; +export * as assertionMessages from './assertion_messages'; diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/error_handling.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/error_handling.spec.ts new file mode 100644 index 0000000000000..914558fbdc97f --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/error_handling.spec.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expect } from '@kbn/scout'; +import { test, testData } from '../fixtures'; + +test.describe('Discover app - errors', { tag: ['@ess'] }, () => { + test.beforeAll(async ({ esArchiver, kbnClient, uiSettings }) => { + await kbnClient.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await esArchiver.loadIfNeeded(testData.ES_ARCHIVES.LOGSTASH); + await kbnClient.importExport.load(testData.KBN_ARCHIVES.INVALID_SCRIPTED_FIELD); + await uiSettings.setDefaultTime({ + from: testData.LOGSTASH_DEFAULT_START_TIME, + to: testData.LOGSTASH_DEFAULT_END_TIME, + }); + }); + + test.afterAll(async ({ kbnClient }) => { + await kbnClient.savedObjects.cleanStandardList(); + }); + + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsViewer(); + await pageObjects.discover.goto(); + }); + + test('should render invalid scripted field error', async ({ page }) => { + await page.testSubj.locator('discoverErrorCalloutTitle').waitFor({ state: 'visible' }); + await expect( + page.testSubj.locator('painlessStackTrace'), + 'Painless error stacktrace should be displayed' + ).toBeVisible(); + }); +}); diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/saved_search_embeddable.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/saved_search_embeddable.spec.ts new file mode 100644 index 0000000000000..7103f2b25e633 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/saved_search_embeddable.spec.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ScoutWorkerFixtures, expect } from '@kbn/scout'; +import { test, testData } from '../fixtures'; + +const createSavedSearch = async ( + kbnClient: ScoutWorkerFixtures['kbnClient'], + searchId: string, + searchTitle: string, + dataViewId: string +) => + await kbnClient.savedObjects.create({ + type: 'search', + id: searchId, + overwrite: false, + attributes: { + title: searchTitle, + description: '', + columns: ['agent', 'bytes', 'clientip'], + sort: [['@timestamp', 'desc']], + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"highlightAll":true,"version":true,"query":{"language":"lucene","query":""},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', + }, + }, + references: [ + { + id: dataViewId, + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + }, + ], + }); + +test.describe( + 'Discover app - saved search embeddable', + { tag: ['@ess', '@svlSecurity', '@svlOblt', '@svlSearch'] }, + () => { + const SAVED_SEARCH_TITLE = 'TempSearch'; + const SAVED_SEARCH_ID = '90943e30-9a47-11e8-b64d-95841ca0b247'; + test.beforeAll(async ({ esArchiver, kbnClient, uiSettings }) => { + await esArchiver.loadIfNeeded(testData.ES_ARCHIVES.LOGSTASH); + await kbnClient.importExport.load(testData.KBN_ARCHIVES.DASHBOARD_DRILLDOWNS); + await uiSettings.set({ + defaultIndex: testData.DATA_VIEW_ID.LOGSTASH, // TODO: investigate why it is required for `node scripts/playwright_test.js` run + 'timepicker:timeDefaults': `{ "from": "${testData.LOGSTASH_DEFAULT_START_TIME}", "to": "${testData.LOGSTASH_DEFAULT_END_TIME}"}`, + }); + }); + + test.afterAll(async ({ kbnClient, uiSettings }) => { + await uiSettings.unset('defaultIndex', 'timepicker:timeDefaults'); + await kbnClient.savedObjects.cleanStandardList(); + }); + + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsPrivilegedUser(); + await pageObjects.dashboard.goto(); + }); + + test('should allow removing the dashboard panel after the underlying saved search has been deleted', async ({ + kbnClient, + page, + pageObjects, + }) => { + await pageObjects.dashboard.openNewDashboard(); + await createSavedSearch( + kbnClient, + SAVED_SEARCH_ID, + SAVED_SEARCH_TITLE, + testData.DATA_VIEW_ID.LOGSTASH + ); + await pageObjects.dashboard.addPanelFromLibrary(SAVED_SEARCH_TITLE); + await page.testSubj.locator('savedSearchTotalDocuments').waitFor({ + state: 'visible', + }); + + await pageObjects.dashboard.saveDashboard('Dashboard with deleted saved search'); + await kbnClient.savedObjects.delete({ + type: 'search', + id: SAVED_SEARCH_ID, + }); + + await page.reload(); + await page.waitForLoadingIndicatorHidden(); + await expect( + page.testSubj.locator('embeddableError'), + 'Embeddable error should be displayed' + ).toBeVisible(); + + await pageObjects.dashboard.removePanel('embeddableError'); + await expect( + page.testSubj.locator('embeddableError'), + 'Embeddable error should not be displayed' + ).toBeHidden(); + }); + } +); diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/saved_searches.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/saved_searches.spec.ts new file mode 100644 index 0000000000000..184c9217bfe15 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/saved_searches.spec.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expect } from '@kbn/scout'; +import { test, testData } from '../fixtures'; +import type { ExtendedScoutTestFixtures } from '../fixtures'; + +const assertNoFilterAndEmptyQuery = async ( + filterBadge: { field: string; value: string }, + pageObjects: ExtendedScoutTestFixtures['pageObjects'], + page: ExtendedScoutTestFixtures['page'] +) => { + expect( + // checking if filter exists, enabled or disabled + await pageObjects.filterBar.hasFilter(filterBadge), + `Filter ${JSON.stringify(filterBadge)} should not exist` + ).toBe(false); + await expect( + page.testSubj.locator('queryInput'), + 'Query Bar input field should be empty' + ).toHaveText(''); +}; + +const assertDataViewIsSelected = async (page: ExtendedScoutTestFixtures['page'], name: string) => + await expect( + page.testSubj.locator('*dataView-switch-link'), + 'Incorrect data view is selected' + ).toHaveText(name); + +test.describe( + 'Discover app - saved searches', + { tag: ['@ess', '@svlSecurity', '@svlOblt', '@svlSearch'] }, + () => { + const START_TIME = '2019-04-27T23:56:51.374Z'; + const END_TIME = '2019-08-23T16:18:51.821Z'; + const PANEL_NAME = 'Ecommerce Data'; + const SEARCH_QUERY = 'customer_gender:MALE'; + const SAVED_SEARCH_NAME = 'test-unselect-saved-search'; + const filterFieldAndValue = { + field: 'category', + value: `Men's Shoes`, + }; + + test.beforeAll(async ({ esArchiver, kbnClient, uiSettings }) => { + await esArchiver.loadIfNeeded(testData.ES_ARCHIVES.ECOMMERCE); + await kbnClient.importExport.load(testData.KBN_ARCHIVES.DISCOVER); + await kbnClient.importExport.load(testData.KBN_ARCHIVES.ECOMMERCE); + await uiSettings.set({ + defaultIndex: testData.DATA_VIEW_ID.ECOMMERCE, + 'doc_table:legacy': false, + 'timepicker:timeDefaults': `{ "from": "${START_TIME}", "to": "${END_TIME}"}`, + }); + }); + + test.afterAll(async ({ kbnClient, uiSettings }) => { + await uiSettings.unset('doc_table:legacy', 'defaultIndex', 'timepicker:timeDefaults'); + await kbnClient.savedObjects.cleanStandardList(); + }); + + test.beforeEach(async ({ browserAuth }) => { + await browserAuth.loginAsPrivilegedUser(); + }); + + test('should customize time range on dashboards', async ({ pageObjects, page }) => { + await pageObjects.dashboard.goto(); + await pageObjects.dashboard.openNewDashboard(); + await pageObjects.dashboard.addPanelFromLibrary(PANEL_NAME); + await page.testSubj.locator('savedSearchTotalDocuments').waitFor({ + state: 'visible', + }); + + await pageObjects.dashboard.customizePanel({ + name: PANEL_NAME, + customTimeRageCommonlyUsed: { value: 'Last_90 days' }, + }); + await expect( + page.testSubj.locator('embeddedSavedSearchDocTable').locator('.euiDataGrid__noResults'), + 'No results message in Saved Search panel should be visible' + ).toBeVisible(); + }); + + test(`should unselect saved search when navigating to a 'new'`, async ({ + pageObjects, + page, + }) => { + await pageObjects.discover.goto(); + await assertDataViewIsSelected(page, testData.DATA_VIEW.ECOMMERCE); + await pageObjects.filterBar.addFilter({ + ...filterFieldAndValue, + operator: 'is', + }); + await page.testSubj.fill('queryInput', SEARCH_QUERY); + await page.testSubj.click('querySubmitButton'); + await pageObjects.discover.waitForHistogramRendered(); + + await pageObjects.discover.saveSearch(SAVED_SEARCH_NAME); + await pageObjects.discover.waitForHistogramRendered(); + + expect( + await pageObjects.filterBar.hasFilter({ + ...filterFieldAndValue, + enabled: true, // Filter is enabled by default + }) + ).toBe(true); + await expect(page.testSubj.locator('queryInput')).toHaveText(SEARCH_QUERY); + + // create new search + await pageObjects.discover.clickNewSearch(); + await assertDataViewIsSelected(page, testData.DATA_VIEW.ECOMMERCE); + await assertNoFilterAndEmptyQuery(filterFieldAndValue, pageObjects, page); + + // change data view + await pageObjects.discover.selectDataView(testData.DATA_VIEW.LOGSTASH); + await assertNoFilterAndEmptyQuery(filterFieldAndValue, pageObjects, page); + + // change data view again + await pageObjects.discover.selectDataView(testData.DATA_VIEW.ECOMMERCE); + await assertNoFilterAndEmptyQuery(filterFieldAndValue, pageObjects, page); + + // create new search again + await pageObjects.discover.clickNewSearch(); + await assertDataViewIsSelected(page, testData.DATA_VIEW.ECOMMERCE); + }); + } +); diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions.spec.ts index ff1389e85924e..f80daeea5b9c7 100644 --- a/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions.spec.ts +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions.spec.ts @@ -6,51 +6,55 @@ */ import { expect } from '@kbn/scout'; -import { test } from '../fixtures'; +import { test, testData, assertionMessages } from '../fixtures'; -test.describe('Discover app - value suggestions', () => { - test.beforeAll(async ({ esArchiver, kbnClient }) => { - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); - await kbnClient.importExport.load( - 'x-pack/test/functional/fixtures/kbn_archiver/dashboard_drilldowns/drilldowns' - ); - await kbnClient.uiSettings.update({ - defaultIndex: 'logstash-*', // TODO: investigate why it is required for `node scripts/playwright_test.js` run - 'doc_table:legacy': false, +test.describe( + 'Discover app - value suggestions: useTimeRange enabled', + { tag: ['@ess', '@svlSecurity', '@svlOblt', '@svlSearch'] }, + () => { + test.beforeAll(async ({ esArchiver, kbnClient, uiSettings }) => { + await esArchiver.loadIfNeeded(testData.ES_ARCHIVES.LOGSTASH); + await kbnClient.importExport.load(testData.KBN_ARCHIVES.DASHBOARD_DRILLDOWNS); + await uiSettings.set({ + defaultIndex: testData.DATA_VIEW_ID.LOGSTASH, // TODO: investigate why it is required for `node scripts/playwright_test.js` run + 'doc_table:legacy': false, + 'timepicker:timeDefaults': `{ "from": "${testData.LOGSTASH_DEFAULT_START_TIME}", "to": "${testData.LOGSTASH_DEFAULT_END_TIME}"}`, + }); }); - }); - test.afterAll(async ({ kbnClient }) => { - await kbnClient.uiSettings.unset('doc_table:legacy'); - await kbnClient.uiSettings.unset('defaultIndex'); - await kbnClient.savedObjects.cleanStandardList(); - }); + test.afterAll(async ({ kbnClient, uiSettings }) => { + await uiSettings.unset('doc_table:legacy', 'defaultIndex', 'timepicker:timeDefaults'); + await kbnClient.savedObjects.cleanStandardList(); + }); - test.beforeEach(async ({ browserAuth, pageObjects }) => { - await browserAuth.loginAsPrivilegedUser(); - await pageObjects.discover.goto(); - }); + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsViewer(); + await pageObjects.discover.goto(); + }); - test('dont show up if outside of range', async ({ page, pageObjects }) => { - await pageObjects.datePicker.setAbsoluteRange({ - from: 'Mar 1, 2020 @ 00:00:00.000', - to: 'Nov 1, 2020 @ 00:00:00.000', + test('dont show up if outside of range', async ({ page, pageObjects }) => { + await pageObjects.datePicker.setAbsoluteRange(testData.LOGSTASH_OUT_OF_RANGE_DATES); + await page.testSubj.fill('queryInput', 'extension.raw : '); + await expect(page.testSubj.locator('autoCompleteSuggestionText')).toHaveCount(0); }); - await page.testSubj.fill('queryInput', 'extension.raw : '); - await expect(page.testSubj.locator('autoCompleteSuggestionText')).toHaveCount(0); - }); + test('show up if in range', async ({ page, pageObjects }) => { + await pageObjects.datePicker.setAbsoluteRange(testData.LOGSTASH_IN_RANGE_DATES); + await page.testSubj.fill('queryInput', 'extension.raw : '); + await expect( + page.testSubj.locator('autoCompleteSuggestionText'), + assertionMessages.QUERY_BAR_VALIDATION.SUGGESTIONS_COUNT + ).toHaveCount(5); + const actualSuggestions = await page.testSubj + .locator('autoCompleteSuggestionText') + .allTextContents(); + expect(actualSuggestions.join(',')).toContain('jpg'); + }); - test('show up if in range', async ({ page, pageObjects }) => { - await pageObjects.datePicker.setAbsoluteRange({ - from: 'Sep 19, 2015 @ 06:31:44.000', - to: 'Sep 23, 2015 @ 18:31:44.000', + test('also displays descriptions for operators', async ({ page, pageObjects }) => { + await pageObjects.datePicker.setAbsoluteRange(testData.LOGSTASH_IN_RANGE_DATES); + await page.testSubj.fill('queryInput', 'extension.raw'); + await expect(page.testSubj.locator('^autocompleteSuggestion-operator')).toHaveCount(2); }); - await page.testSubj.fill('queryInput', 'extension.raw : '); - await expect(page.testSubj.locator('autoCompleteSuggestionText')).toHaveCount(5); - const actualSuggestions = await page.testSubj - .locator('autoCompleteSuggestionText') - .allTextContents(); - expect(actualSuggestions.join(',')).toContain('jpg'); - }); -}); + } +); diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_non_time_based.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_non_time_based.spec.ts index 4ba9450869313..0e4591ffd9e1e 100644 --- a/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_non_time_based.spec.ts +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_non_time_based.spec.ts @@ -6,39 +6,43 @@ */ import { expect } from '@kbn/scout'; -import { test } from '../fixtures'; +import { test, testData, assertionMessages } from '../fixtures'; -test.describe('Discover app - value suggestions non-time based', () => { - test.beforeAll(async ({ esArchiver, kbnClient }) => { - await esArchiver.loadIfNeeded( - 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' - ); - await kbnClient.importExport.load( - 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' - ); - await kbnClient.uiSettings.update({ - defaultIndex: 'without-timefield', - 'doc_table:legacy': false, +test.describe( + 'Discover app - value suggestions non-time based', + { tag: ['@ess', '@svlSecurity', '@svlOblt', '@svlSearch'] }, + () => { + test.beforeAll(async ({ esArchiver, kbnClient, uiSettings }) => { + await esArchiver.loadIfNeeded(testData.ES_ARCHIVES.NO_TIME_FIELD); + await kbnClient.importExport.load(testData.KBN_ARCHIVES.NO_TIME_FIELD); + await uiSettings.set({ + defaultIndex: 'without-timefield', + 'doc_table:legacy': false, + }); }); - }); - test.afterAll(async ({ kbnClient }) => { - await kbnClient.uiSettings.unset('doc_table:legacy'); - await kbnClient.uiSettings.unset('defaultIndex'); - await kbnClient.savedObjects.cleanStandardList(); - }); + test.afterAll(async ({ kbnClient, uiSettings }) => { + await uiSettings.unset('doc_table:legacy', 'defaultIndex'); + await kbnClient.savedObjects.cleanStandardList(); + }); - test.beforeEach(async ({ browserAuth, pageObjects }) => { - await browserAuth.loginAsPrivilegedUser(); - await pageObjects.discover.goto(); - }); + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsViewer(); + await pageObjects.discover.goto(); + }); - test('shows all auto-suggest options for a filter in discover context app', async ({ page }) => { - await page.testSubj.fill('queryInput', 'type.keyword : '); - await expect(page.testSubj.locator('autoCompleteSuggestionText')).toHaveCount(1); - const actualSuggestions = await page.testSubj - .locator('autoCompleteSuggestionText') - .allTextContents(); - expect(actualSuggestions.join(',')).toContain('"apache"'); - }); -}); + test('shows all auto-suggest options for a filter in discover context app', async ({ + page, + }) => { + await page.testSubj.fill('queryInput', 'type.keyword : '); + await expect( + page.testSubj.locator('autoCompleteSuggestionText'), + assertionMessages.QUERY_BAR_VALIDATION.SUGGESTIONS_COUNT + ).toHaveCount(1); + const actualSuggestions = await page.testSubj + .locator('autoCompleteSuggestionText') + .allTextContents(); + expect(actualSuggestions.join(',')).toContain('"apache"'); + }); + } +); diff --git a/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_use_time_range_disabled.spec.ts b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_use_time_range_disabled.spec.ts new file mode 100644 index 0000000000000..c5f0ad9b43ef6 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_use_time_range_disabled.spec.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { expect } from '@kbn/scout'; +import { test, testData, assertionMessages } from '../fixtures'; + +test.describe( + 'Discover app - value suggestions: useTimeRange disabled', + { tag: ['@ess', '@svlSecurity', '@svlOblt', '@svlSearch'] }, + () => { + test.beforeAll(async ({ esArchiver, kbnClient, uiSettings }) => { + await esArchiver.loadIfNeeded(testData.ES_ARCHIVES.LOGSTASH); + await kbnClient.importExport.load(testData.KBN_ARCHIVES.DASHBOARD_DRILLDOWNS); + await uiSettings.set({ + defaultIndex: testData.DATA_VIEW_ID.LOGSTASH, // TODO: investigate why it is required for `node scripts/playwright_test.js` run + 'doc_table:legacy': false, + 'timepicker:timeDefaults': `{ "from": "${testData.LOGSTASH_DEFAULT_START_TIME}", "to": "${testData.LOGSTASH_DEFAULT_END_TIME}"}`, + 'autocomplete:useTimeRange': false, + }); + }); + + test.afterAll(async ({ uiSettings, kbnClient }) => { + await uiSettings.unset('doc_table:legacy', 'defaultIndex', 'timepicker:timeDefaults'); + await uiSettings.set({ 'autocomplete:useTimeRange': true }); + await kbnClient.savedObjects.cleanStandardList(); + }); + + test.beforeEach(async ({ browserAuth, pageObjects }) => { + await browserAuth.loginAsViewer(); + await pageObjects.discover.goto(); + }); + + test('show up if outside of range', async ({ page, pageObjects }) => { + await pageObjects.datePicker.setAbsoluteRange(testData.LOGSTASH_OUT_OF_RANGE_DATES); + await page.testSubj.fill('queryInput', 'extension.raw : '); + await expect( + page.testSubj.locator('autoCompleteSuggestionText'), + assertionMessages.QUERY_BAR_VALIDATION.SUGGESTIONS_COUNT + ).toHaveCount(5); + const actualSuggestions = await page.testSubj + .locator('autoCompleteSuggestionText') + .allTextContents(); + expect(actualSuggestions.join(',')).toContain('jpg'); + }); + + test('show up if in range', async ({ page, pageObjects }) => { + await pageObjects.datePicker.setAbsoluteRange(testData.LOGSTASH_IN_RANGE_DATES); + await page.testSubj.fill('queryInput', 'extension.raw : '); + await expect( + page.testSubj.locator('autoCompleteSuggestionText'), + assertionMessages.QUERY_BAR_VALIDATION.SUGGESTIONS_COUNT + ).toHaveCount(5); + const actualSuggestions = await page.testSubj + .locator('autoCompleteSuggestionText') + .allTextContents(); + expect(actualSuggestions.join(',')).toContain('jpg'); + }); + } +);