[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 <traeluki@gmail.com>
This commit is contained in:
Dzmitry Lemechko 2024-12-02 20:57:29 +01:00 committed by GitHub
parent a674b9d043
commit d4094c17be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 974 additions and 166 deletions

View file

@ -16,4 +16,7 @@ export type {
PageObjects,
ScoutTestFixtures,
ScoutWorkerFixtures,
EsArchiverFixture,
} from './src/playwright';
export type { Client, KbnClient, KibanaUrl, SamlSessionManager, ToolingLog } from './src/types';

View file

@ -15,6 +15,7 @@ import { scoutTestFixtures } from './test';
export const scoutCoreFixtures = mergeTests(scoutWorkerFixtures, scoutTestFixtures);
export type {
EsArchiverFixture,
ScoutTestFixtures,
ScoutWorkerFixtures,
ScoutPage,

View file

@ -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<keyof Page> = [
'check',
'click',
@ -28,10 +28,15 @@ function extendPageWithTestSubject(page: Page) {
'innerText',
'isChecked',
'isHidden',
'isVisible',
'locator',
'waitForSelector',
];
const extendedMethods: Partial<Record<keyof Page, Function>> = {};
const extendedMethods: Partial<Record<keyof Page, Function>> & {
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<keyof Page, any>;
// 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);
},
});

View file

@ -22,8 +22,30 @@ export interface LoginFixture {
loginAsPrivilegedUser: () => Promise<void>;
}
/**
* 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<Page['goto']>[1]) => ReturnType<Page['goto']>;
/**
* Waits for the Kibana loading spinner indicator to disappear.
* @returns A Promise resolving when the indicator is hidden.
*/
waitForLoadingIndicatorHidden: () => ReturnType<Page['waitForSelector']>;
/**
* Simplified API to interact with elements using Kibana's 'data-test-subj' attribute.
*/
testSubj: {
check: (selector: string, options?: Parameters<Page['check']>[1]) => ReturnType<Page['check']>;
click: (selector: string, options?: Parameters<Page['click']>[1]) => ReturnType<Page['click']>;
@ -59,9 +81,34 @@ export type ScoutPage = Page & {
selector: string,
options?: Parameters<Page['isHidden']>[1]
) => ReturnType<Page['isHidden']>;
isVisible: (
selector: string,
options?: Parameters<Page['isVisible']>[1]
) => ReturnType<Page['isVisible']>;
locator: (
selector: string,
options?: Parameters<Page['locator']>[1]
) => ReturnType<Page['locator']>;
waitForSelector: (
selector: string,
options?: Parameters<Page['waitForSelector']>[1]
) => ReturnType<Page['waitForSelector']>;
// 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<void>;
/**
* 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<void>;
};
};

View file

@ -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<Record<string, IndexStats>>;
}
export interface UiSettingsFixture {
set: (values: UiSettingValues) => Promise<void>;
unset: (...values: string[]) => Promise<any>;
setDefaultTime: ({ from, to }: { from: string; to: string }) => Promise<void>;
}
export interface ScoutWorkerFixtures {
log: ToolingLog;
config: ScoutServerConfig;
kbnUrl: KibanaUrl;
esClient: Client;
kbnClient: KbnClient;
uiSettings: UiSettingsFixture;
esArchiver: EsArchiverFixture;
samlAuth: SamlSessionManager;
}

View file

@ -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' },
],
});

View file

@ -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);

View file

@ -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' },
],
});

View file

@ -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';

View file

@ -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',
});
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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 });
}
}

View file

@ -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
};
}

View file

@ -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();
}

View file

@ -10,3 +10,4 @@
export * from './config';
export * from './cli';
export * from './servers';
export * from './services';

View file

@ -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';

View file

@ -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: <div> data-test-subj="bar-foo" </div>
* - DOM match example: <div data-test-subj="bar-foo"> </div>
*
* - 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: <div data-test-subj="foo_bar"> </div>
*
* - 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: <div> data-test-subj="foo bar" </div>
* - DOM match example: <div data-test-subj="foo bar"> </div>
*
* - 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:
* <div> data-test-subj="foo"
* <div> data-test-subj="bar" </div>
* <div data-test-subj="foo">
* <div data-test-subj="bar"> </div>
* </div>
*
* - 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: <div> data-test-subj="foo bar" </div>
* - DOM match example: <div data-test-subj="foo bar"> </div>
*/
export function subj(selector: string) {
return selectorToTerms(selector)

View file

@ -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`

View file

@ -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',
};

View file

@ -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',
};

View file

@ -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<ExtendedScoutTestFixtures, ScoutWorkerFixtures>(
await use(extendedPageObjects);
},
});
export * as testData from './constants';
export * as assertionMessages from './assertion_messages';

View file

@ -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();
});
});

View file

@ -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();
});
}
);

View file

@ -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);
});
}
);

View file

@ -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.afterAll(async ({ kbnClient }) => {
await kbnClient.uiSettings.unset('doc_table:legacy');
await kbnClient.uiSettings.unset('defaultIndex');
await kbnClient.savedObjects.cleanStandardList();
});
test.beforeEach(async ({ browserAuth, pageObjects }) => {
await browserAuth.loginAsPrivilegedUser();
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.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}"}`,
});
});
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({
from: 'Sep 19, 2015 @ 06:31:44.000',
to: 'Sep 23, 2015 @ 18:31:44.000',
test.afterAll(async ({ kbnClient, uiSettings }) => {
await uiSettings.unset('doc_table:legacy', 'defaultIndex', 'timepicker:timeDefaults');
await kbnClient.savedObjects.cleanStandardList();
});
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');
});
});
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(testData.LOGSTASH_OUT_OF_RANGE_DATES);
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('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);
});
}
);

View file

@ -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"');
});
}
);

View file

@ -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');
});
}
);