Scout: run tests in parallel (with spaces) (#207253)

## Summary

This PR adds `spaceTest` interface to `kbn-scout` to run space aware
tests, that can be executed in parallel. Most of Discover tests were
converted to parallel run because we see runtime improvement with 2
parallel workers.

Experiment 1: **ES data pre-ingested**, running 9 Discover **stateful**
tests in **5 files** locally
| Run setup  | Took time |
| ------------- | ------------- |
| 1 worker  | `1.3` min |
| 2 workers | `58.7` sec  |
| 3 workers | `48.3` sec  |
| 4 workers | **tests fail**  |

Conclusion: using **2** workers is the optimal solution to continue

Experiment 2: Running Discover tests for stateful/serverless in **Kibana
CI** (starting servers, ingesting ES data, running tests)
| Run setup  | 1 worker | 2 workers | diff
| ------------- | ------------- |------------- |------------- |
| stateful, 9 tests / 5 files  | `1.7` min | `1.2` min | `-29.4%`|
| svl ES, 8 tests / 4 files  | `1.7` min | `1.3` min | `-23.5%`|
| svl Oblt, 8 tests / 4 files  | `1.8` min | `1.4` min | `-22.2%`|
| svl Search, 5 tests / 2 files  | `59.9` sec | `51.6` sec | `-13.8%`|

Conclusion: parallel run effectiveness benefits from tests being split
in **more test files**.

Experiment 3: Clone existing tests to have **3 times more test files**
and re-run tests for stateful/serverless in **Kibana CI** (starting
servers, ingesting ES data, running tests)
| Run setup  | 1 worker | 2 workers | diff
| ------------- | ------------- |------------- |------------- |
| stateful, 27 tests / 15 files  | `4.3` min | `2.7` min | `-37.2%`|
| svl ES, 24 tests / 12 files  | `4.3` min | `2.7` min | `-37.2%`|

Conclusion: parallel run effectiveness is **increasing** with more test
files in place, **not linear** but with good test design we can expect
**up to 40%** or maybe a bit more.

How parallel run works:
- `scoutSpace` fixture is loaded on Playwright worker setup (using
`auto: true` config), creates a new Kibana Space, expose its id to other
fixtures and deletes the space on teardown.
- `browserAuth` fixture for parallel run caches Cookie per worker/space
like `role:spaceId`. It is needed because Playwright doesn't spin up new
browser for worker, but only new context.
- kbnClient was updated to allow passing `createNewCopies: true` in
query, it is needed to load the same Saved Objects in parallel
workers/spaces and generate new ids to work with them. `scoutSpace`
caches ids and allows to reach saved object by its name. This logic is
different from single thread run, where we can use default ids from
kbnArchives.

How to run parallel tests locally, e.g. for stateful: 
```
node scripts/scout run-tests --stateful --config x-pack/platform/plugins/private/discover_enhanced/ui_tests/parallel.playwright.config.ts
```
This commit is contained in:
Dzmitry Lemechko 2025-01-23 20:09:06 +01:00 committed by GitHub
parent d7f801ab3e
commit 14c3235182
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1085 additions and 445 deletions

View file

@ -26,6 +26,7 @@ EXIT_CODE=0
# Discovery Enhanced
for run_mode in "--stateful" "--serverless=es" "--serverless=oblt" "--serverless=security"; do
run_tests "Discovery Enhanced: Parallel Workers" "x-pack/platform/plugins/private/discover_enhanced/ui_tests/parallel.playwright.config.ts" "$run_mode"
run_tests "Discovery Enhanced" "x-pack/platform/plugins/private/discover_enhanced/ui_tests/playwright.config.ts" "$run_mode"
done

View file

@ -8,15 +8,24 @@
*/
export * as cli from './src/cli';
export { expect, test, tags, createPlaywrightConfig, createLazyPageObject } from './src/playwright';
export {
expect,
test,
spaceTest,
tags,
createPlaywrightConfig,
createLazyPageObject,
ingestTestDataHook,
} from './src/playwright';
export type {
ScoutPage,
ScoutPlaywrightOptions,
ScoutTestOptions,
ScoutPage,
PageObjects,
ScoutTestFixtures,
ScoutWorkerFixtures,
EsArchiverFixture,
ScoutParallelTestFixtures,
ScoutParallelWorkerFixtures,
} from './src/playwright';
export type { Client, KbnClient, KibanaUrl, SamlSessionManager, ToolingLog } from './src/types';

View file

@ -55,8 +55,9 @@ export class KibanaUrl {
* @param appName name of the app to get the URL for
* @param options optional modifications to apply to the URL
*/
app(appName: string, options?: PathOptions) {
return this.get(`/app/${appName}`, options);
app(appName: string, options?: { space?: string; pathOptions?: PathOptions }) {
const relPath = options?.space ? `s/${options.space}/app/${appName}` : `/app/${appName}`;
return this.get(relPath, options?.pathOptions);
}
toString() {

View file

@ -21,6 +21,7 @@ export function createPlaywrightConfig(options: ScoutPlaywrightOptions): Playwri
return defineConfig<ScoutTestOptions>({
testDir: options.testDir,
globalSetup: options.globalSetup,
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */

View file

@ -7,20 +7,5 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { mergeTests } from '@playwright/test';
import { scoutWorkerFixtures } from './worker';
import { scoutTestFixtures } from './test';
export const scoutCoreFixtures = mergeTests(scoutWorkerFixtures, scoutTestFixtures);
export type {
EsArchiverFixture,
ScoutTestFixtures,
ScoutWorkerFixtures,
ScoutPage,
Client,
KbnClient,
KibanaUrl,
ToolingLog,
} from './types';
export * from './single_thread_fixtures';
export * from './parallel_run_fixtures';

View file

@ -0,0 +1,52 @@
/*
* 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 { mergeTests } from 'playwright/test';
import { coreWorkerFixtures, scoutSpaceParallelFixture } from './worker';
import type {
EsClient,
KbnClient,
KibanaUrl,
ScoutSpaceParallelFixture,
ScoutTestConfig,
ToolingLog,
} from './worker';
import {
scoutPageParallelFixture,
browserAuthParallelFixture,
pageObjectsParallelFixture,
validateTagsFixture,
} from './test';
import type { BrowserAuthFixture, ScoutPage, PageObjects } from './test';
export const scoutParallelFixtures = mergeTests(
// worker scope fixtures
coreWorkerFixtures,
scoutSpaceParallelFixture,
// test scope fixtures
browserAuthParallelFixture,
scoutPageParallelFixture,
pageObjectsParallelFixture,
validateTagsFixture
);
export interface ScoutParallelTestFixtures {
browserAuth: BrowserAuthFixture;
page: ScoutPage;
pageObjects: PageObjects;
}
export interface ScoutParallelWorkerFixtures {
log: ToolingLog;
config: ScoutTestConfig;
kbnUrl: KibanaUrl;
kbnClient: KbnClient;
esClient: EsClient;
scoutSpace: ScoutSpaceParallelFixture;
}

View file

@ -0,0 +1,58 @@
/*
* 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 { mergeTests } from 'playwright/test';
import { coreWorkerFixtures, esArchiverFixture, uiSettingsFixture } from './worker';
import type {
EsArchiverFixture,
EsClient,
KbnClient,
KibanaUrl,
ScoutTestConfig,
ToolingLog,
UiSettingsFixture,
} from './worker';
import {
scoutPageFixture,
browserAuthFixture,
pageObjectsFixture,
validateTagsFixture,
BrowserAuthFixture,
ScoutPage,
PageObjects,
} from './test';
export type { PageObjects, ScoutPage } from './test';
export const scoutFixtures = mergeTests(
// worker scope fixtures
coreWorkerFixtures,
esArchiverFixture,
uiSettingsFixture,
// test scope fixtures
browserAuthFixture,
scoutPageFixture,
pageObjectsFixture,
validateTagsFixture
);
export interface ScoutTestFixtures {
browserAuth: BrowserAuthFixture;
page: ScoutPage;
pageObjects: PageObjects;
}
export interface ScoutWorkerFixtures {
log: ToolingLog;
config: ScoutTestConfig;
kbnUrl: KibanaUrl;
kbnClient: KbnClient;
esClient: EsClient;
esArchiver: EsArchiverFixture;
uiSettings: UiSettingsFixture;
}

View file

@ -0,0 +1,31 @@
/*
* 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 LoginFunction = (role: string) => Promise<void>;
export interface BrowserAuthFixture {
/**
* Logs in as a user with viewer-only permissions.
* @returns A Promise that resolves once the cookie in browser is set.
*/
loginAsViewer: () => Promise<void>;
/**
* Logs in as a user with administrative privileges
* @returns A Promise that resolves once the cookie in browser is set.
*/
loginAsAdmin: () => Promise<void>;
/**
* Logs in as a user with elevated, but not admin, permissions.
* @returns A Promise that resolves once the cookie in browser is set.
*/
loginAsPrivilegedUser: () => Promise<void>;
}
export { browserAuthParallelFixture } from './parallel';
export { browserAuthFixture } from './single_thread';

View file

@ -0,0 +1,56 @@
/*
* 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 { BrowserAuthFixture, LoginFunction } from '.';
import { PROJECT_DEFAULT_ROLES } from '../../../../common';
import { serviceLoadedMsg } from '../../../utils';
import { coreWorkerFixtures } from '../../worker';
import { ScoutSpaceParallelFixture } from '../../worker/scout_space';
/**
* The "browserAuth" fixture simplifies the process of logging into Kibana with
* different roles during tests. It uses the "samlAuth" fixture to create an authentication session
* for the specified role and the "context" fixture to update the cookie with the role-scoped session.
*/
export const browserAuthParallelFixture = coreWorkerFixtures.extend<
{ browserAuth: BrowserAuthFixture },
{ scoutSpace: ScoutSpaceParallelFixture }
>({
browserAuth: async ({ log, context, samlAuth, config, scoutSpace }, use) => {
const setSessionCookie = async (cookieValue: string) => {
await context.clearCookies();
await context.addCookies([
{
name: 'sid',
value: cookieValue,
path: '/',
domain: 'localhost',
},
]);
};
const loginAs: LoginFunction = async (role) => {
const spaceId = scoutSpace.id;
const cookie = await samlAuth.getInteractiveUserSessionCookieWithRoleScope(role, { spaceId });
await setSessionCookie(cookie);
};
const loginAsAdmin = () => loginAs('admin');
const loginAsViewer = () => loginAs('viewer');
const loginAsPrivilegedUser = () => {
const roleName = config.serverless
? PROJECT_DEFAULT_ROLES.get(config.projectType!)!
: 'editor';
return loginAs(roleName);
};
log.debug(serviceLoadedMsg(`browserAuth:${scoutSpace.id}`));
await use({ loginAsAdmin, loginAsViewer, loginAsPrivilegedUser });
},
});

View file

@ -7,19 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { test as base } from '@playwright/test';
import { PROJECT_DEFAULT_ROLES } from '../../../common';
import { LoginFixture, ScoutWorkerFixtures } from '../types';
import { serviceLoadedMsg } from '../../utils';
type LoginFunction = (role: string) => Promise<void>;
import { PROJECT_DEFAULT_ROLES } from '../../../../common';
import { serviceLoadedMsg } from '../../../utils';
import { coreWorkerFixtures } from '../../worker';
import { BrowserAuthFixture, LoginFunction } from '.';
/**
* The "browserAuth" fixture simplifies the process of logging into Kibana with
* different roles during tests. It uses the "samlAuth" fixture to create an authentication session
* for the specified role and the "context" fixture to update the cookie with the role-scoped session.
*/
export const browserAuthFixture = base.extend<{ browserAuth: LoginFixture }, ScoutWorkerFixtures>({
export const browserAuthFixture = coreWorkerFixtures.extend<{ browserAuth: BrowserAuthFixture }>({
browserAuth: async ({ log, context, samlAuth, config }, use) => {
const setSessionCookie = async (cookieValue: string) => {
await context.clearCookies();

View file

@ -7,15 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { mergeTests } from '@playwright/test';
import { browserAuthFixture } from './browser_auth';
import { scoutPageFixture } from './page';
import { pageObjectsFixture } from './page_objects';
import { validateTagsFixture } from './validate_tags';
export const scoutTestFixtures = mergeTests(
browserAuthFixture,
scoutPageFixture,
pageObjectsFixture,
validateTagsFixture
);
export { browserAuthFixture, browserAuthParallelFixture } from './browser_auth';
export type { BrowserAuthFixture } from './browser_auth';
export { scoutPageFixture, scoutPageParallelFixture } from './scout_page';
export type { ScoutPage } from './scout_page';
export { validateTagsFixture } from './validate_tags';
export { pageObjectsFixture, pageObjectsParallelFixture } from './page_objects';
export type { PageObjects } from './page_objects';

View file

@ -0,0 +1,15 @@
/*
* 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 LoginFunction = (role: string) => Promise<void>;
export type { PageObjects } from '../../../page_objects';
export { pageObjectsParallelFixture } from './parallel';
export { pageObjectsFixture } from './single_thread';

View file

@ -0,0 +1,34 @@
/*
* 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 { PageObjects, createCorePageObjects } from '../../../page_objects';
import { serviceLoadedMsg } from '../../../utils';
import { ScoutSpaceParallelFixture } from '../../worker';
import { scoutPageParallelFixture } from '../scout_page';
/**
* The "pageObjects" fixture provides a centralized and consistent way to access and
* interact with reusable Page Objects in tests. This fixture automatically
* initializes core Page Objects and makes them available to tests, promoting
* modularity and reducing redundant setup.
*
* Note: Page Objects are lazily instantiated on first access.
*/
export const pageObjectsParallelFixture = scoutPageParallelFixture.extend<
{
pageObjects: PageObjects;
},
{ scoutSpace: ScoutSpaceParallelFixture }
>({
pageObjects: async ({ page, log, scoutSpace }, use) => {
const corePageObjects = createCorePageObjects(page);
log.debug(serviceLoadedMsg(`pageObjects:${scoutSpace.id}`));
await use(corePageObjects);
},
});

View file

@ -7,9 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { test as base } from '@playwright/test';
import { ScoutTestFixtures, ScoutWorkerFixtures } from '../types';
import { createCorePageObjects } from '../../page_objects';
import { PageObjects, createCorePageObjects } from '../../../page_objects';
import { serviceLoadedMsg } from '../../../utils';
import { scoutPageFixture } from '../scout_page';
/**
* The "pageObjects" fixture provides a centralized and consistent way to access and
@ -19,10 +19,12 @@ import { createCorePageObjects } from '../../page_objects';
*
* Note: Page Objects are lazily instantiated on first access.
*/
export const pageObjectsFixture = base.extend<ScoutTestFixtures, ScoutWorkerFixtures>({
pageObjects: async ({ page }, use) => {
export const pageObjectsFixture = scoutPageFixture.extend<{
pageObjects: PageObjects;
}>({
pageObjects: async ({ page, log }, use) => {
const corePageObjects = createCorePageObjects(page);
log.debug(serviceLoadedMsg(`pageObjects`));
await use(corePageObjects);
},
});

View file

@ -7,32 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Page } from 'playwright/test';
import { PageObjects } from '../../page_objects';
export interface ScoutTestFixtures {
browserAuth: LoginFixture;
page: ScoutPage;
pageObjects: PageObjects;
}
export interface LoginFixture {
/**
* Logs in as a user with viewer-only permissions.
* @returns A Promise that resolves once the cookie in browser is set.
*/
loginAsViewer: () => Promise<void>;
/**
* Logs in as a user with administrative privileges
* @returns A Promise that resolves once the cookie in browser is set.
*/
loginAsAdmin: () => Promise<void>;
/**
* Logs in as a user with elevated, but not admin, permissions.
* @returns A Promise that resolves once the cookie in browser is set.
*/
loginAsPrivilegedUser: () => Promise<void>;
}
import { Page } from '@playwright/test';
/**
* Extends the Playwright 'Page' interface with methods specific to Kibana.
@ -124,3 +99,6 @@ export type ScoutPage = Page & {
clearInput: (selector: string) => Promise<void>;
};
};
export { scoutPageFixture } from './single_thread';
export { scoutPageParallelFixture } from './parallel';

View file

@ -0,0 +1,39 @@
/*
* 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 { Page, test as base } from '@playwright/test';
import { ScoutPage } from '.';
import { KibanaUrl, ToolingLog } from '../../worker';
import { ScoutSpaceParallelFixture } from '../../worker/scout_space';
import { extendPlaywrightPage } from './single_thread';
import { serviceLoadedMsg } from '../../../utils';
export const scoutPageParallelFixture = base.extend<
{ page: ScoutPage },
{ log: ToolingLog; kbnUrl: KibanaUrl; scoutSpace: ScoutSpaceParallelFixture }
>({
page: async (
{
log,
page,
kbnUrl,
scoutSpace,
}: { log: ToolingLog; page: Page; kbnUrl: KibanaUrl; scoutSpace: ScoutSpaceParallelFixture },
use: (extendedPage: ScoutPage) => Promise<void>
) => {
const extendedPage = extendPlaywrightPage({ page, kbnUrl });
// Overriding navigation to specific Kibana apps: url should respect the Kibana Space id
extendedPage.gotoApp = (appName: string) =>
page.goto(kbnUrl.app(appName, { space: scoutSpace.id }));
log.debug(serviceLoadedMsg(`scoutPage:${scoutSpace.id}`));
await use(extendedPage);
},
});

View file

@ -7,9 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Page, test as base } from '@playwright/test';
import { Page } from '@playwright/test';
import { subj } from '@kbn/test-subj-selector';
import { ScoutPage, KibanaUrl, ScoutTestFixtures, ScoutWorkerFixtures } from '../types';
import { KibanaUrl, ToolingLog, coreWorkerFixtures } from '../../worker';
import { ScoutPage } from '.';
import { serviceLoadedMsg } from '../../../utils';
/**
* Instead of defining each method individually, we use a list of method names and loop through them, creating methods dynamically.
@ -69,6 +71,26 @@ function extendPageWithTestSubject(page: Page): ScoutPage['testSubj'] {
return extendedMethods as ScoutPage['testSubj'];
}
export function extendPlaywrightPage({
page,
kbnUrl,
}: {
page: Page;
kbnUrl: KibanaUrl;
}): ScoutPage {
const extendedPage = page as ScoutPage;
// Extend page with '@kbn/test-subj-selector' support
extendedPage.testSubj = extendPageWithTestSubject(page);
// Method to navigate to specific Kibana apps
extendedPage.gotoApp = (appName: string) => page.goto(kbnUrl.app(appName));
// Method to wait for global loading indicator to be hidden
extendedPage.waitForLoadingIndicatorHidden = () =>
extendedPage.testSubj.waitForSelector('globalLoadingIndicator-hidden', {
state: 'attached',
});
return extendedPage;
}
/**
* Extends the 'page' fixture with Kibana-specific functionality
*
@ -95,20 +117,17 @@ function extendPageWithTestSubject(page: Page): ScoutPage['testSubj'] {
* await page.gotoApp('discover);
* ```
*/
export const scoutPageFixture = base.extend<ScoutTestFixtures, ScoutWorkerFixtures>({
export const scoutPageFixture = coreWorkerFixtures.extend<
{ page: ScoutPage; log: ToolingLog },
{ kbnUrl: KibanaUrl }
>({
page: async (
{ page, kbnUrl }: { page: Page; kbnUrl: KibanaUrl },
{ page, kbnUrl, log }: { page: Page; kbnUrl: KibanaUrl; log: ToolingLog },
use: (extendedPage: ScoutPage) => Promise<void>
) => {
const extendedPage = page as ScoutPage;
// Extend page with '@kbn/test-subj-selector' support
extendedPage.testSubj = extendPageWithTestSubject(page);
// Method to navigate to specific Kibana apps
extendedPage.gotoApp = (appName: string) => page.goto(kbnUrl.app(appName));
// Method to wait for global loading indicator to be hidden
extendedPage.waitForLoadingIndicatorHidden = () =>
extendedPage.testSubj.waitForSelector('globalLoadingIndicator-hidden', { state: 'attached' });
const extendedPage = extendPlaywrightPage({ page, kbnUrl });
log.debug(serviceLoadedMsg(`scoutPage`));
await use(extendedPage);
},
});

View file

@ -8,7 +8,7 @@
*/
import { test as base } from '@playwright/test';
import { tags } from '../../tags';
import { tags } from '../../../tags';
const supportedTags = tags.DEPLOYMENT_AGNOSTIC;

View file

@ -1,74 +0,0 @@
/*
* 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 type { KbnClient, SamlSessionManager } from '@kbn/test';
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 { ScoutTestConfig } from '../../../types';
import { KibanaUrl } from '../../../common/services/kibana_url';
export interface EsArchiverFixture {
/**
* Loads an Elasticsearch archive if the specified data index is not present.
* @param name The name of the archive to load.
* @param performance An object of type LoadActionPerfOptions to measure and
* report performance metrics during the load operation.
* @returns A Promise that resolves to an object containing index statistics.
*/
loadIfNeeded: (
name: string,
performance?: LoadActionPerfOptions | undefined
) => Promise<Record<string, IndexStats>>;
}
export interface UiSettingsFixture {
/**
* Applies one or more UI settings
* @param values (UiSettingValues): An object containing key-value pairs of UI settings to apply.
* @returns A Promise that resolves once the settings are applied.
*/
set: (values: UiSettingValues) => Promise<void>;
/**
* Resets specific UI settings to their default values.
* @param values A list of UI setting keys to unset.
* @returns A Promise that resolves after the settings are unset.
*/
unset: (...values: string[]) => Promise<any>;
/**
* Sets the default time range for Kibana.
* @from The start time of the default time range.
* @to The end time of the default time range.
* @returns A Promise that resolves once the default time is set.
*/
setDefaultTime: ({ from, to }: { from: string; to: string }) => Promise<void>;
}
/**
* The `ScoutWorkerFixtures` type defines the set of fixtures that are available
*/
export interface ScoutWorkerFixtures {
log: ToolingLog;
config: ScoutTestConfig;
kbnUrl: KibanaUrl;
esClient: Client;
kbnClient: KbnClient;
uiSettings: UiSettingsFixture;
esArchiver: EsArchiverFixture;
samlAuth: SamlSessionManager;
}
// re-export to import types from '@kbn-scout'
export type { KbnClient, SamlSessionManager } from '@kbn/test';
export type { ToolingLog } from '@kbn/tooling-log';
export type { Client } from '@elastic/elasticsearch';
export type { KibanaUrl } from '../../../common/services/kibana_url';

View file

@ -9,18 +9,28 @@
import { test as base } from '@playwright/test';
import { LoadActionPerfOptions } from '@kbn/es-archiver';
import type { ToolingLog } from '@kbn/tooling-log';
import { KbnClient, SamlSessionManager } from '@kbn/test';
import { Client } from '@elastic/elasticsearch';
import {
createKbnUrl,
createEsArchiver,
createEsClient,
createKbnClient,
createLogger,
createSamlSessionManager,
createScoutConfig,
KibanaUrl,
} from '../../../common/services';
import { ScoutWorkerFixtures } from '../types/worker_scope';
import { ScoutTestOptions } from '../../types';
import { ScoutTestConfig } from '.';
// re-export to import types from '@kbn-scout'
export type { KbnClient, SamlSessionManager } from '@kbn/test';
export type { ToolingLog } from '@kbn/tooling-log';
export type { Client as EsClient } from '@elastic/elasticsearch';
export type { KibanaUrl } from '../../../common/services/kibana_url';
export type { ScoutTestConfig } from '../../../types';
/**
* The coreWorkerFixtures setup defines foundational fixtures that are essential
@ -29,7 +39,17 @@ import { ScoutTestOptions } from '../../types';
* and isolated access to critical services such as logging, configuration, and
* clients for interacting with Kibana and Elasticsearch.
*/
export const coreWorkerFixtures = base.extend<{}, ScoutWorkerFixtures>({
export const coreWorkerFixtures = base.extend<
{},
{
log: ToolingLog;
config: ScoutTestConfig;
kbnUrl: KibanaUrl;
esClient: Client;
kbnClient: KbnClient;
samlAuth: SamlSessionManager;
}
>({
// Provides a scoped logger instance for each worker. This logger is shared across
// all other fixtures within the worker scope.
log: [
@ -87,25 +107,6 @@ export const coreWorkerFixtures = base.extend<{}, ScoutWorkerFixtures>({
{ scope: 'worker' },
],
/**
* Provides utilities for managing test data in Elasticsearch. The "loadIfNeeded" method
* optimizes test execution by loading data archives only if required, avoiding redundant
* data ingestion.
*
* Note: In order to speedup test execution and avoid the overhead of deleting the data
* we only expose capability to ingest the data indexes.
*/
esArchiver: [
({ log, esClient, kbnClient }, use) => {
const esArchiverInstance = createEsArchiver(esClient, kbnClient, log);
const loadIfNeeded = async (name: string, performance?: LoadActionPerfOptions | undefined) =>
esArchiverInstance!.loadIfNeeded(name, performance);
use({ loadIfNeeded });
},
{ scope: 'worker' },
],
/**
* Creates a SAML session manager, that handles authentication tasks for tests involving
* SAML-based authentication.

View file

@ -0,0 +1,48 @@
/*
* 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 { LoadActionPerfOptions } from '@kbn/es-archiver';
import { IndexStats } from '@kbn/es-archiver/src/lib/stats';
import { coreWorkerFixtures } from './core_fixtures';
import { createEsArchiver } from '../../../common/services';
export interface EsArchiverFixture {
/**
* Loads an Elasticsearch archive if the specified data index is not present.
* @param name The name of the archive to load.
* @param performance An object of type LoadActionPerfOptions to measure and
* report performance metrics during the load operation.
* @returns A Promise that resolves to an object containing index statistics.
*/
loadIfNeeded: (
name: string,
performance?: LoadActionPerfOptions | undefined
) => Promise<Record<string, IndexStats>>;
}
export const esArchiverFixture = coreWorkerFixtures.extend<{}, { esArchiver: EsArchiverFixture }>({
/**
* Provides utilities for managing test data in Elasticsearch. The "loadIfNeeded" method
* optimizes test execution by loading data archives only if required, avoiding redundant
* data ingestion.
*
* Note: In order to speedup test execution and avoid the overhead of deleting the data
* we only expose capability to ingest the data indexes.
*/
esArchiver: [
({ log, esClient, kbnClient }, use) => {
const esArchiverInstance = createEsArchiver(esClient, kbnClient, log);
const loadIfNeeded = async (name: string, performance?: LoadActionPerfOptions | undefined) =>
esArchiverInstance!.loadIfNeeded(name, performance);
use({ loadIfNeeded });
},
{ scope: 'worker' },
],
});

View file

@ -7,8 +7,21 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { mergeTests } from 'playwright/test';
import { uiSettingsFixture } from './ui_settings';
import { coreWorkerFixtures } from './core';
export { coreWorkerFixtures } from './core_fixtures';
export type {
ToolingLog,
ScoutTestConfig,
KibanaUrl,
EsClient,
KbnClient,
SamlSessionManager,
} from './core_fixtures';
export const scoutWorkerFixtures = mergeTests(coreWorkerFixtures, uiSettingsFixture);
export { esArchiverFixture } from './es_archiver';
export type { EsArchiverFixture } from './es_archiver';
export { uiSettingsFixture } from './ui_settings';
export type { UiSettingsFixture } from './ui_settings';
export { scoutSpaceParallelFixture } from './scout_space';
export type { ScoutSpaceParallelFixture } from './scout_space';

View file

@ -0,0 +1,40 @@
/*
* 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 { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings';
export interface ImportSavedObjects {
type: string;
destinationId: string;
meta: { title: string };
}
export interface ImportExportResponse {
successResults: ImportSavedObjects[];
}
export interface SavedObjectResponse {
id: string;
type: string;
title: string;
}
export interface ScoutSpaceParallelFixture {
id: string;
savedObjects: {
load: (path: string) => Promise<SavedObjectResponse[]>;
cleanStandardList: () => Promise<void>;
};
uiSettings: {
setDefaultIndex: (dataViewName: string) => Promise<void>;
set: (values: UiSettingValues) => Promise<void>;
unset: (...keys: string[]) => Promise<any[]>;
setDefaultTime: ({ from, to }: { from: string; to: string }) => Promise<void>;
};
}
export { scoutSpaceParallelFixture } from './parallel';

View file

@ -0,0 +1,134 @@
/*
* 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 { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings';
import { formatTime, isValidUTCDate, measurePerformance, serviceLoadedMsg } from '../../../utils';
import { coreWorkerFixtures } from '..';
import { ImportSavedObjects, ScoutSpaceParallelFixture } from '.';
export const scoutSpaceParallelFixture = coreWorkerFixtures.extend<
{},
{ scoutSpace: ScoutSpaceParallelFixture }
>({
scoutSpace: [
async ({ log, kbnClient }, use, workerInfo) => {
const spaceId = `test-space-${workerInfo.workerIndex}`;
const spacePayload = {
id: spaceId,
name: spaceId,
disabledFeatures: [],
};
await measurePerformance(log, `scoutSpace:${spaceId} 'spaces.create'`, async () => {
return kbnClient.spaces.create(spacePayload);
});
// cache saved objects ids in space
const savedObjectsCache = new Map<string, string>();
const load = async (path: string) => {
return measurePerformance(log, `scoutSpace:${spaceId} 'savedObjects.load'`, async () => {
const response = await kbnClient.importExport.load(path, {
space: spaceId,
// will create new copies of saved objects with unique ids
createNewCopies: true,
});
const imported = (response.successResults as ImportSavedObjects[]).map(
(r: { type: string; destinationId: string; meta: { title: string } }) => {
return { id: r.destinationId, type: r.type, title: r.meta.title };
}
);
imported.forEach((so) => savedObjectsCache.set(so.title, so.id));
return imported;
});
};
const cleanStandardList = async () => {
return measurePerformance(
log,
`scoutSpace:${spaceId} 'savedObjects.cleanStandardList'`,
async () => {
savedObjectsCache.clear();
await kbnClient.savedObjects.cleanStandardList({
space: spaceId,
});
}
);
};
const setDefaultIndex = async (dataViewName: string) => {
return measurePerformance(
log,
`scoutSpace:${spaceId} 'savedObjects.setDefaultIndex'`,
async () => {
if (savedObjectsCache.has(dataViewName)) {
return kbnClient.uiSettings.update(
{
defaultIndex: savedObjectsCache.get(dataViewName)!,
},
{ space: spaceId }
);
} else {
throw new Error(`Data view id ${dataViewName} not found in space ${spaceId}`);
}
}
);
};
const set = async (values: UiSettingValues) => {
return measurePerformance(log, `scoutSpace:${spaceId} 'uiSettings.set'`, async () => {
return kbnClient.uiSettings.update(values, { space: spaceId });
});
};
const unset = async (...keys: string[]) => {
return measurePerformance(log, `scoutSpace:${spaceId} 'uiSettings.unset'`, async () => {
return Promise.all(
keys.map((key) => kbnClient.uiSettings.unset(key, { space: spaceId }))
);
});
};
const setDefaultTime = async ({ from, to }: { from: string; to: string }) => {
return measurePerformance(
log,
`scoutSpace:${spaceId} 'uiSettings.setDefaultTime'`,
async () => {
const utcFrom = isValidUTCDate(from) ? from : formatTime(from);
const untcTo = isValidUTCDate(to) ? to : formatTime(to);
return kbnClient.uiSettings.update(
{
'timepicker:timeDefaults': `{ "from": "${utcFrom}", "to": "${untcTo}"}`,
},
{ space: spaceId }
);
}
);
};
const savedObjects = {
load,
cleanStandardList,
};
const uiSettings = {
setDefaultIndex,
set,
unset,
setDefaultTime,
};
log.debug(serviceLoadedMsg(`scoutSpace:${spaceId}`));
await use({ savedObjects, uiSettings, id: spaceId });
// Cleanup space after tests via API call
await measurePerformance(log, `scoutSpace:${spaceId} 'space.delete'`, async () => {
return kbnClient.spaces.delete(spaceId);
});
},
{ scope: 'worker', auto: true },
],
});

View file

@ -0,0 +1,34 @@
/*
* 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 { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings';
export interface UiSettingsFixture {
/**
* Applies one or more UI settings
* @param values (UiSettingValues): An object containing key-value pairs of UI settings to apply.
* @returns A Promise that resolves once the settings are applied.
*/
set: (values: UiSettingValues) => Promise<void>;
/**
* Resets specific UI settings to their default values.
* @param values A list of UI setting keys to unset.
* @returns A Promise that resolves after the settings are unset.
*/
unset: (...values: string[]) => Promise<any>;
/**
* Sets the default time range for Kibana.
* @from The start time of the default time range.
* @to The end time of the default time range.
* @returns A Promise that resolves once the default time is set.
*/
setDefaultTime: ({ from, to }: { from: string; to: string }) => Promise<void>;
}
export { uiSettingsFixture } from './single_thread';

View file

@ -7,19 +7,21 @@
* 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';
import { isValidUTCDate, formatTime, serviceLoadedMsg } from '../../../utils';
import { coreWorkerFixtures } from '../core_fixtures';
import { UiSettingsFixture } from '.';
/**
* This fixture provides a way to interact with Kibana UI settings.
*/
export const uiSettingsFixture = base.extend<{}, ScoutWorkerFixtures>({
export const uiSettingsFixture = coreWorkerFixtures.extend<{}, { uiSettings: UiSettingsFixture }>({
uiSettings: [
({ kbnClient }, use) => {
async ({ kbnClient, log }, use) => {
const kbnClientUiSettings = {
set: async (values: UiSettingValues) => kbnClient.uiSettings.update(values),
set: async (values: UiSettingValues) => {
await kbnClient.uiSettings.update(values);
},
unset: async (...keys: string[]) =>
Promise.all(keys.map((key) => kbnClient.uiSettings.unset(key))),
@ -33,7 +35,8 @@ export const uiSettingsFixture = base.extend<{}, ScoutWorkerFixtures>({
},
};
use(kbnClientUiSettings);
log.debug(serviceLoadedMsg(`uiSettings`));
await use(kbnClientUiSettings);
},
{ scope: 'worker' },
],

View file

@ -0,0 +1,45 @@
/*
* 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 { FullConfig } from 'playwright/test';
import {
createEsArchiver,
createEsClient,
createKbnClient,
createLogger,
createScoutConfig,
} from '../../common';
import { ScoutTestOptions } from '../types';
import { measurePerformance } from '../utils';
export async function ingestTestDataHook(config: FullConfig, archives: string[]) {
const log = createLogger();
if (archives.length === 0) {
log.info('[scout setup] no test data to ingest');
return;
}
return measurePerformance(log, '[scout setup]: ingestTestDataHook', async () => {
// TODO: This should be configurable local vs cloud
const configName = 'local';
const projectUse = config.projects[0].use as ScoutTestOptions;
const serversConfigDir = projectUse.serversConfigDir;
const scoutConfig = createScoutConfig(serversConfigDir, configName, log);
const esClient = createEsClient(scoutConfig, log);
const kbnCLient = createKbnClient(scoutConfig, log);
const esArchiver = createEsArchiver(esClient, kbnCLient, log);
log.info('[scout setup] loading test data (only if indexes do not exist)...');
for (const archive of archives) {
await esArchiver.loadIfNeeded(archive);
}
});
}

View file

@ -7,5 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './test_scope';
export * from './worker_scope';
export { ingestTestDataHook } from './data_ingestion';

View file

@ -7,24 +7,29 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { mergeTests } from 'playwright/test';
import { scoutCoreFixtures } from './fixtures';
import { scoutFixtures, scoutParallelFixtures } from './fixtures';
// Scout core fixtures: worker & test scope
export const test = mergeTests(scoutCoreFixtures);
export const test = scoutFixtures;
// Scout core 'space aware' fixtures: worker & test scope
export const spaceTest = scoutParallelFixtures;
export { createPlaywrightConfig } from './config';
export { createLazyPageObject } from './page_objects/utils';
export { expect } from './expect';
export type { ScoutPlaywrightOptions, ScoutTestOptions } from './types';
export type { PageObjects } from './page_objects';
export type {
ScoutTestFixtures,
ScoutWorkerFixtures,
ScoutParallelTestFixtures,
ScoutParallelWorkerFixtures,
ScoutPage,
EsArchiverFixture,
PageObjects,
} from './fixtures';
// use to tag tests
export { tags } from './tags';
export { ingestTestDataHook } from './global_hooks';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../fixtures/types';
import { ScoutPage } from '..';
type CommonlyUsedTimeRange =
| 'Today'

View file

@ -7,8 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../fixtures/types';
import { expect } from '..';
import { ScoutPage, expect } from '..';
export class DatePicker {
constructor(private readonly page: ScoutPage) {}

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../fixtures/types';
import { ScoutPage } from '..';
export class DiscoverApp {
constructor(private readonly page: ScoutPage) {}

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../fixtures/types';
import { ScoutPage } from '..';
import { expect } from '..';
interface FilterCreationOptions {

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../fixtures/types';
import { ScoutPage } from '..';
import { DashboardApp } from './dashboard_app';
import { DatePicker } from './date_picker';
import { DiscoverApp } from './discover_app';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../../fixtures/types';
import { ScoutPage } from '../../fixtures';
/**
* Creates a lazily instantiated proxy for a Page Object class, deferring the creation of the instance until

View file

@ -18,7 +18,9 @@ export interface ScoutTestOptions extends PlaywrightTestOptions {
[VALID_CONFIG_MARKER]: boolean;
}
export interface ScoutPlaywrightOptions extends Pick<PlaywrightTestConfig, 'testDir' | 'workers'> {
export interface ScoutPlaywrightOptions
extends Pick<PlaywrightTestConfig, 'testDir' | 'workers' | 'globalSetup'> {
testDir: string;
workers?: 1 | 2;
workers?: 1 | 2 | 3; // to keep performance consistent within test suites
globalSetup?: string;
}

View file

@ -8,3 +8,4 @@
*/
export { serviceLoadedMsg, isValidUTCDate, formatTime, getPlaywrightGrepTag } from './runner_utils';
export { measurePerformance } from './performance';

View file

@ -0,0 +1,22 @@
/*
* 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 { ToolingLog } from '../../types';
export const measurePerformance = async <T>(
log: ToolingLog,
label: string,
fn: () => Promise<T>
): Promise<T> => {
const startTime = performance.now();
const result = await fn();
const duration = performance.now() - startTime;
log.debug(`${label} took ${duration.toFixed(2)}ms`);
return result;
};

View file

@ -11,7 +11,7 @@ import moment from 'moment';
import { Config } from '../../config';
import { tagsByMode } from '../tags';
export const serviceLoadedMsg = (name: string) => `scout service loaded: ${name}`;
export const serviceLoadedMsg = (name: string) => `[scout service] ${name}`;
export const isValidUTCDate = (date: string): boolean => {
return !isNaN(Date.parse(date)) && new Date(date).toISOString() === date;

View file

@ -41,7 +41,8 @@ export interface SupportedRoles {
}
export interface GetCookieOptions {
forceNewSession: boolean;
forceNewSession?: boolean;
spaceId?: string;
}
/**
@ -120,18 +121,20 @@ Set env variable 'TEST_CLOUD=1' to run FTR against your Cloud deployment`
};
private getSessionByRole = async (options: GetSessionByRole): Promise<Session> => {
const { role, forceNewSession } = options;
const { role, forceNewSession, spaceId } = options;
// Validate role before creating SAML session
this.validateRole(role);
const cacheKey = spaceId ? `${role}:${spaceId}` : role;
// Check if session is cached and not forced to create the new one
if (!forceNewSession && this.sessionCache.has(role)) {
return this.sessionCache.get(role)!;
if (!forceNewSession && this.sessionCache.has(cacheKey)) {
return this.sessionCache.get(cacheKey)!;
}
const session = await this.createSessionForRole(role);
this.sessionCache.set(role, session);
this.sessionCache.set(cacheKey, session);
if (forceNewSession) {
this.log.debug(`Session for role '${role}' was force updated.`);
@ -181,13 +184,20 @@ Set env variable 'TEST_CLOUD=1' to run FTR against your Cloud deployment`
async getApiCredentialsForRole(role: string, options?: GetCookieOptions) {
const { forceNewSession } = options || { forceNewSession: false };
const session = await this.getSessionByRole({ role, forceNewSession });
const session = await this.getSessionByRole({
role,
forceNewSession: forceNewSession ?? false,
});
return { Cookie: `sid=${session.getCookieValue()}` };
}
async getInteractiveUserSessionCookieWithRoleScope(role: string, options?: GetCookieOptions) {
const { forceNewSession } = options || { forceNewSession: false };
const session = await this.getSessionByRole({ role, forceNewSession });
const forceNewSession = options?.forceNewSession ?? false;
const session = await this.getSessionByRole({
role,
forceNewSession,
spaceId: options?.spaceId,
});
return session.getCookieValue();
}

View file

@ -65,4 +65,5 @@ export interface RetryParams {
export interface GetSessionByRole {
role: string;
forceNewSession: boolean;
spaceId?: string;
}

View file

@ -55,7 +55,7 @@ export async function runCheckFtrConfigsCli() {
}
// playwright config files
if (file.match(/\/ui_tests\/*playwright*.config.ts$/)) {
if (file.match(/\/*playwright*.config.ts$/)) {
return false;
}

View file

@ -55,7 +55,7 @@ export class KbnClientImportExport {
return absolutePath;
}
async load(path: string, options?: { space?: string }) {
async load(path: string, options?: { space?: string; createNewCopies?: boolean }) {
const src = this.resolveAndValidatePath(path);
this.log.debug('resolved import for', path, 'to', src);
@ -65,19 +65,20 @@ export class KbnClientImportExport {
const formData = new FormData();
formData.append('file', objects.map((obj) => JSON.stringify(obj)).join('\n'), 'import.ndjson');
const query = options?.createNewCopies ? { createNewCopies: true } : { overwrite: true };
// TODO: should we clear out the existing saved objects?
const resp = await this.req<ImportApiResponse>(options?.space, {
method: 'POST',
path: '/api/saved_objects/_import',
query: {
overwrite: true,
},
query,
body: formData,
headers: formData.getHeaders(),
});
if (resp.data.success) {
this.log.success('import success');
return resp.data;
} else {
throw createFailError(
`failed to import all saved objects: ${inspect(resp.data, {

View file

@ -8,14 +8,24 @@
export const LOGSTASH_DEFAULT_START_TIME = '2015-09-19T06:31:44.000Z';
export const LOGSTASH_DEFAULT_END_TIME = '2015-09-23T18:31:44.000Z';
/**
* Should be used in "single thread" tests to set default Data View
* @example uiSettings.set({ defaultIndex: testData.DATA_VIEW_ID.ECOMMERCE });
*/
export const DATA_VIEW_ID = {
ECOMMERCE: '5193f870-d861-11e9-a311-0fa548c5f953',
LOGSTASH: 'logstash-*',
NO_TIME_FIELD: 'c1e8af24-c7b7-4d9b-ab0e-e408c88d29c9',
};
export const DATA_VIEW = {
/**
* Should be used in "parallel tests" to set default Data View, because ids are generated and can't be hardcoded
* @example scoutSpace.uiSettings.setDefaultIndex(testData.DATA_VIEW_NAME.ECOMMERCE);
*/
export const DATA_VIEW_NAME = {
ECOMMERCE: 'ecommerce',
LOGSTASH: 'logstash-*',
NO_TIME_FIELD: 'without-timefield',
};
export const LOGSTASH_OUT_OF_RANGE_DATES = {

View file

@ -6,30 +6,62 @@
*/
import {
test as base,
test as baseTest,
spaceTest as spaceBaseTest,
PageObjects,
createLazyPageObject,
ScoutTestFixtures,
ScoutWorkerFixtures,
ScoutParallelTestFixtures,
ScoutParallelWorkerFixtures,
} from '@kbn/scout';
import { DemoPage } from './page_objects';
export interface ExtendedScoutTestFixtures extends ScoutTestFixtures {
export interface ExtScoutTestFixtures extends ScoutTestFixtures {
pageObjects: PageObjects & {
demo: DemoPage;
};
}
export const test = base.extend<ExtendedScoutTestFixtures, ScoutWorkerFixtures>({
export const test = baseTest.extend<ExtScoutTestFixtures, ScoutWorkerFixtures>({
pageObjects: async (
{
pageObjects,
page,
}: {
pageObjects: ExtendedScoutTestFixtures['pageObjects'];
page: ExtendedScoutTestFixtures['page'];
pageObjects: ExtScoutTestFixtures['pageObjects'];
page: ExtScoutTestFixtures['page'];
},
use: (pageObjects: ExtendedScoutTestFixtures['pageObjects']) => Promise<void>
use: (pageObjects: ExtScoutTestFixtures['pageObjects']) => Promise<void>
) => {
const extendedPageObjects = {
...pageObjects,
demo: createLazyPageObject(DemoPage, page),
};
await use(extendedPageObjects);
},
});
export interface ExtParallelRunTestFixtures extends ScoutParallelTestFixtures {
pageObjects: PageObjects & {
demo: DemoPage;
};
}
export const spaceTest = spaceBaseTest.extend<
ExtParallelRunTestFixtures,
ScoutParallelWorkerFixtures
>({
pageObjects: async (
{
pageObjects,
page,
}: {
pageObjects: ExtParallelRunTestFixtures['pageObjects'];
page: ExtParallelRunTestFixtures['page'];
},
use: (pageObjects: ExtParallelRunTestFixtures['pageObjects']) => Promise<void>
) => {
const extendedPageObjects = {
...pageObjects,

View file

@ -0,0 +1,15 @@
/*
* 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 { createPlaywrightConfig } from '@kbn/scout';
// eslint-disable-next-line import/no-default-export
export default createPlaywrightConfig({
globalSetup: require.resolve('./parallel_tests/global_setup'),
testDir: './parallel_tests',
workers: 2,
});

View file

@ -6,29 +6,28 @@
*/
import { expect, tags } from '@kbn/scout';
import { test, testData } from '../fixtures';
import { spaceTest, testData } from '../fixtures';
test.describe('Discover app - errors', { tag: tags.ESS_ONLY }, () => {
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({
spaceTest.describe('Discover app - errors', { tag: tags.ESS_ONLY }, () => {
spaceTest.beforeAll(async ({ scoutSpace }) => {
await scoutSpace.savedObjects.cleanStandardList();
await scoutSpace.savedObjects.load(testData.KBN_ARCHIVES.INVALID_SCRIPTED_FIELD);
await scoutSpace.uiSettings.setDefaultTime({
from: testData.LOGSTASH_DEFAULT_START_TIME,
to: testData.LOGSTASH_DEFAULT_END_TIME,
});
});
test.afterAll(async ({ kbnClient }) => {
await kbnClient.savedObjects.cleanStandardList();
spaceTest.afterAll(async ({ scoutSpace }) => {
await scoutSpace.savedObjects.cleanStandardList();
});
test.beforeEach(async ({ browserAuth, pageObjects }) => {
spaceTest.beforeEach(async ({ browserAuth, pageObjects }) => {
await browserAuth.loginAsViewer();
await pageObjects.discover.goto();
});
test('should render invalid scripted field error', async ({ page }) => {
spaceTest('should render invalid scripted field error', async ({ page }) => {
await page.testSubj.locator('discoverErrorCalloutTitle').waitFor({ state: 'visible' });
await expect(
page.testSubj.locator('painlessStackTrace'),

View file

@ -0,0 +1,24 @@
/*
* 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 { ingestTestDataHook } from '@kbn/scout';
import { type FullConfig } from '@playwright/test';
import { testData } from '../fixtures';
async function globalSetup(config: FullConfig) {
// add archives to load, if needed
const archives = [
testData.ES_ARCHIVES.LOGSTASH,
testData.ES_ARCHIVES.NO_TIME_FIELD,
testData.ES_ARCHIVES.ECOMMERCE,
];
return ingestTestDataHook(config, archives);
}
// eslint-disable-next-line import/no-default-export
export default globalSetup;

View file

@ -0,0 +1,127 @@
/*
* 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 { spaceTest, testData } from '../fixtures';
import type { ExtParallelRunTestFixtures } from '../fixtures';
const assertNoFilterAndEmptyQuery = async (
filterBadge: { field: string; value: string },
pageObjects: ExtParallelRunTestFixtures['pageObjects'],
page: ExtParallelRunTestFixtures['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: ExtParallelRunTestFixtures['page'], name: string) =>
await expect(
page.testSubj.locator('*dataView-switch-link'),
'Incorrect data view is selected'
).toHaveText(name);
spaceTest.describe(
'Discover app - saved searches',
{ tag: ['@ess', '@svlSearch', '@svlOblt'] },
() => {
// TODO: Update to use an ES archive with an index accessible to 'viewer'
// for running this test against the Security serverless project.
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`,
};
spaceTest.beforeAll(async ({ scoutSpace }) => {
await scoutSpace.savedObjects.load(testData.KBN_ARCHIVES.DISCOVER);
await scoutSpace.savedObjects.load(testData.KBN_ARCHIVES.ECOMMERCE);
await scoutSpace.uiSettings.setDefaultIndex(testData.DATA_VIEW_NAME.ECOMMERCE);
await scoutSpace.uiSettings.setDefaultTime({ from: START_TIME, to: END_TIME });
});
spaceTest.afterAll(async ({ scoutSpace }) => {
await scoutSpace.uiSettings.unset('defaultIndex', 'timepicker:timeDefaults');
await scoutSpace.savedObjects.cleanStandardList();
});
spaceTest.beforeEach(async ({ browserAuth }) => {
await browserAuth.loginAsPrivilegedUser();
});
spaceTest('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();
});
spaceTest(
`should unselect saved search when navigating to a 'new'`,
async ({ pageObjects, page }) => {
await pageObjects.discover.goto();
await assertDataViewIsSelected(page, testData.DATA_VIEW_NAME.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_NAME.ECOMMERCE);
await assertNoFilterAndEmptyQuery(filterFieldAndValue, pageObjects, page);
// change data view
await pageObjects.discover.selectDataView(testData.DATA_VIEW_NAME.LOGSTASH);
await assertNoFilterAndEmptyQuery(filterFieldAndValue, pageObjects, page);
// change data view again
await pageObjects.discover.selectDataView(testData.DATA_VIEW_NAME.ECOMMERCE);
await assertNoFilterAndEmptyQuery(filterFieldAndValue, pageObjects, page);
// create new search again
await pageObjects.discover.clickNewSearch();
await assertDataViewIsSelected(page, testData.DATA_VIEW_NAME.ECOMMERCE);
}
);
}
);

View file

@ -6,38 +6,38 @@
*/
import { expect, tags } from '@kbn/scout';
import { test, testData, assertionMessages } from '../fixtures';
import { spaceTest, testData, assertionMessages } from '../fixtures';
test.describe(
spaceTest.describe(
'Discover app - value suggestions: useTimeRange enabled',
{ tag: tags.DEPLOYMENT_AGNOSTIC },
() => {
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}"}`,
spaceTest.beforeAll(async ({ scoutSpace }) => {
await scoutSpace.savedObjects.load(testData.KBN_ARCHIVES.DASHBOARD_DRILLDOWNS);
await scoutSpace.uiSettings.setDefaultIndex(testData.DATA_VIEW_NAME.LOGSTASH);
await scoutSpace.uiSettings.setDefaultTime({
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();
spaceTest.afterAll(async ({ scoutSpace }) => {
await scoutSpace.uiSettings.unset('defaultIndex', 'timepicker:timeDefaults');
await scoutSpace.savedObjects.cleanStandardList();
});
test.beforeEach(async ({ browserAuth, pageObjects }) => {
spaceTest.beforeEach(async ({ browserAuth, pageObjects }) => {
await browserAuth.loginAsViewer();
await pageObjects.discover.goto();
});
test('dont show up if outside of range', async ({ page, pageObjects }) => {
spaceTest('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 }) => {
spaceTest('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(
@ -50,7 +50,7 @@ test.describe(
expect(actualSuggestions.join(',')).toContain('jpg');
});
test('also displays descriptions for operators', async ({ page, pageObjects }) => {
spaceTest('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

@ -0,0 +1,47 @@
/*
* 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 { spaceTest, testData, assertionMessages } from '../fixtures';
spaceTest.describe(
'Discover app - value suggestions non-time based',
{ tag: ['@ess', '@svlSearch', '@svlOblt'] },
// TODO: Update to use an ES archive with an index accessible to 'viewer'
// for running this test against the Security serverless project.
() => {
spaceTest.beforeAll(async ({ scoutSpace }) => {
await scoutSpace.savedObjects.load(testData.KBN_ARCHIVES.NO_TIME_FIELD);
await scoutSpace.uiSettings.setDefaultIndex(testData.DATA_VIEW_NAME.NO_TIME_FIELD);
});
spaceTest.afterAll(async ({ scoutSpace }) => {
await scoutSpace.uiSettings.unset('defaultIndex');
await scoutSpace.savedObjects.cleanStandardList();
});
spaceTest.beforeEach(async ({ browserAuth, pageObjects }) => {
await browserAuth.loginAsViewer();
await pageObjects.discover.goto();
});
spaceTest(
'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

@ -6,34 +6,34 @@
*/
import { expect, tags } from '@kbn/scout';
import { test, testData, assertionMessages } from '../fixtures';
import { spaceTest, testData, assertionMessages } from '../fixtures';
test.describe(
spaceTest.describe(
'Discover app - value suggestions: useTimeRange disabled',
{ tag: tags.DEPLOYMENT_AGNOSTIC },
() => {
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}"}`,
'autocomplete:useTimeRange': false,
spaceTest.beforeAll(async ({ scoutSpace }) => {
await scoutSpace.savedObjects.load(testData.KBN_ARCHIVES.DASHBOARD_DRILLDOWNS);
await scoutSpace.uiSettings.setDefaultIndex(testData.DATA_VIEW_NAME.LOGSTASH);
await scoutSpace.uiSettings.setDefaultTime({
from: testData.LOGSTASH_DEFAULT_START_TIME,
to: testData.LOGSTASH_DEFAULT_END_TIME,
});
await scoutSpace.uiSettings.set({ 'autocomplete:useTimeRange': false });
});
test.afterAll(async ({ uiSettings, kbnClient }) => {
await uiSettings.unset('defaultIndex', 'timepicker:timeDefaults');
await uiSettings.set({ 'autocomplete:useTimeRange': true });
await kbnClient.savedObjects.cleanStandardList();
spaceTest.afterAll(async ({ scoutSpace }) => {
await scoutSpace.uiSettings.unset('defaultIndex', 'timepicker:timeDefaults');
await scoutSpace.uiSettings.set({ 'autocomplete:useTimeRange': true });
await scoutSpace.savedObjects.cleanStandardList();
});
test.beforeEach(async ({ browserAuth, pageObjects }) => {
spaceTest.beforeEach(async ({ browserAuth, pageObjects }) => {
await browserAuth.loginAsViewer();
await pageObjects.discover.goto();
});
test('show up if outside of range', async ({ page, pageObjects }) => {
spaceTest('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(
@ -46,7 +46,7 @@ test.describe(
expect(actualSuggestions.join(',')).toContain('jpg');
});
test('show up if in range', async ({ page, pageObjects }) => {
spaceTest('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(

View file

@ -1,123 +0,0 @@
/*
* 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', '@svlSearch', '@svlOblt'] }, () => {
// TODO: Update to use an ES archive with an index accessible to 'viewer'
// for running this test against the Security serverless project.
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,
'timepicker:timeDefaults': `{ "from": "${START_TIME}", "to": "${END_TIME}"}`,
});
});
test.afterAll(async ({ kbnClient, uiSettings }) => {
await uiSettings.unset('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

@ -1,49 +0,0 @@
/*
* 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 non-time based',
{ tag: ['@ess', '@svlSearch', '@svlOblt'] },
// TODO: Update to use an ES archive with an index accessible to 'viewer'
// for running this test against the Security serverless project.
() => {
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',
});
});
test.afterAll(async ({ kbnClient, uiSettings }) => {
await uiSettings.unset('defaultIndex');
await kbnClient.savedObjects.cleanStandardList();
});
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'),
assertionMessages.QUERY_BAR_VALIDATION.SUGGESTIONS_COUNT
).toHaveCount(1);
const actualSuggestions = await page.testSubj
.locator('autoCompleteSuggestionText')
.allTextContents();
expect(actualSuggestions.join(',')).toContain('"apache"');
});
}
);