[kbn-scout] initial package implementation (playwright-test POC) (#194006)

## Summary

This PR introduces a new package, **kbn-scout**, designed to streamline
the setup and execution of Playwright tests for Kibana. The `kbn-scout`
package consolidates server management and testing capabilities by
wrapping both the Kibana/Elasticsearch server launcher and the
Playwright test runner. It includes:

- Test and worker-scoped fixtures for reliable setup across test suites
- Page objects combined into the fixture for Kibana UI interactions
- Configurations for seamless test execution in both local and CI
environments (`Cloud` execution is out of scope)
- This package aims to simplify test setup and enhance modularity,
making it easier to create, run, and maintain deployment-agnostic tests,
that are located in the plugin they actually test.

Tests example is available in `x-pack/plugins/discover_enhanced` plugin
under `ui_tests` folder

How to run:
1) As a single script (servers + tests): 
```
node scripts/scout_test.js --config=x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts --serverless=es
```

2) Start servers first
```
node scripts/scout_start_servers.js --serverless=es
```
then run tests:
```
npx playwright test --config=x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts
```
if you have Playwright plugin in IDEA, just use it to run tests files

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dzmitry Lemechko 2024-11-26 17:33:49 +01:00 committed by GitHub
parent a92103b2a9
commit 310d922f13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
86 changed files with 3081 additions and 3 deletions

2
.github/CODEOWNERS vendored
View file

@ -466,6 +466,7 @@ packages/kbn-rrule @elastic/response-ops
packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/response-ops @elastic/obs-ux-management-team
packages/kbn-safer-lodash-set @elastic/kibana-security
packages/kbn-saved-objects-settings @elastic/appex-sharedux
packages/kbn-scout @elastic/appex-qa
packages/kbn-screenshotting-server @elastic/appex-sharedux
packages/kbn-search-api-keys-components @elastic/search-kibana
packages/kbn-search-api-keys-server @elastic/search-kibana
@ -1552,6 +1553,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
/.eslintignore @elastic/kibana-operations
# QA - Appex QA
/x-pack/plugins/discover_enhanced/ui_tests/ @elastic/appex-qa # temporarily
/x-pack/test/functional/fixtures/package_registry_config.yml @elastic/appex-qa # No usages found
/x-pack/test/functional/fixtures/kbn_archiver/packaging.json @elastic/appex-qa # No usages found
/x-pack/test/functional/es_archives/filebeat @elastic/appex-qa

2
.gitignore vendored
View file

@ -143,6 +143,8 @@ x-pack/test/security_api_integration/plugins/audit_log/audit.log
.ftr
role_users.json
# ignore Scout temp directory
.scout
.devcontainer/.env

View file

@ -1484,6 +1484,7 @@
"@kbn/repo-path": "link:packages/kbn-repo-path",
"@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier",
"@kbn/repo-source-classifier-cli": "link:packages/kbn-repo-source-classifier-cli",
"@kbn/scout": "link:packages/kbn-scout",
"@kbn/security-api-integration-helpers": "link:x-pack/test/security_api_integration/packages/helpers",
"@kbn/serverless-storybook-config": "link:packages/serverless/storybook/config",
"@kbn/some-dev-log": "link:packages/kbn-some-dev-log",

View file

@ -58,5 +58,6 @@ export const TEST_DIR = new Set([
'storybook',
'.storybook',
'integration_tests',
'ui_tests',
...RANDOM_TEST_FILE_NAMES,
]);

View file

@ -0,0 +1,9 @@
# @kbn/scout
The package is designed to streamline the setup and execution of Playwright tests for Kibana. It consolidates server management and testing capabilities by wrapping both the Kibana/Elasticsearch server launcher and the Playwright test runner. It includes:
- core test and worker-scoped fixtures for reliable setup across test suites
- page objects combined into the fixture for for core Kibana apps UI interactions
- configurations for seamless test execution in both local and CI environments
This package aims to simplify test setup and enhance modularity, making it easier to create, run, and maintain deployment-agnostic tests, that are located in the plugin they actually test.

View file

@ -0,0 +1,19 @@
/*
* 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 { startServersCli, runTestsCli } from './src/cli';
export { expect, test, createPlaywrightConfig, createLazyPageObject } from './src/playwright';
export type {
ScoutPage,
ScoutPlaywrightOptions,
ScoutTestOptions,
PageObjects,
ScoutTestFixtures,
ScoutWorkerFixtures,
} from './src/playwright';

View file

@ -0,0 +1,14 @@
/*
* 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".
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-scout'],
};

View file

@ -0,0 +1,6 @@
{
"type": "test-helper",
"id": "@kbn/scout",
"owner": "@elastic/appex-qa",
"devOnly": true
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/scout",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -0,0 +1,11 @@
/*
* 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 { runTestsCli } from './run_tests_cli';
export { startServersCli } from './start_servers_cli';

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 { run } from '@kbn/dev-cli-runner';
import { initLogsDir } from '@kbn/test';
import { TEST_FLAG_OPTIONS, parseTestFlags, runTests } from '../playwright/runner';
/**
* Start servers and run the tests
*/
export function runTestsCli() {
run(
async ({ flagsReader, log }) => {
const options = await parseTestFlags(flagsReader);
if (options.logsDir) {
initLogsDir(log, options.logsDir);
}
await runTests(log, options);
},
{
description: `Run Scout UI Tests`,
usage: `
Usage:
node scripts/scout_test --help
node scripts/scout_test --stateful --config <playwright_config_path>
node scripts/scout_test --serverless=es --headed --config <playwright_config_path>
`,
flags: TEST_FLAG_OPTIONS,
}
);
}

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 { run } from '@kbn/dev-cli-runner';
import { initLogsDir } from '@kbn/test';
import { startServers, parseServerFlags, SERVER_FLAG_OPTIONS } from '../servers';
/**
* Start servers
*/
export function startServersCli() {
run(
async ({ flagsReader: flags, log }) => {
const options = parseServerFlags(flags);
if (options.logsDir) {
initLogsDir(log, options.logsDir);
}
await startServers(log, options);
},
{
flags: SERVER_FLAG_OPTIONS,
}
);
}

View file

@ -0,0 +1,16 @@
/*
* 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 { Role } from '@kbn/test/src/auth/types';
export const PROJECT_DEFAULT_ROLES = new Map<string, Role>([
['es', 'developer'],
['security', 'editor'],
['oblt', 'editor'],
]);

View file

@ -0,0 +1,12 @@
/*
* 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 * from './services';
export * from './constants';
export * from './utils';

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 { KbnClient, createEsClientForTesting } from '@kbn/test';
import type { ToolingLog } from '@kbn/tooling-log';
import { ScoutServerConfig } from '../../types';
import { serviceLoadedMsg } from '../../playwright/utils';
interface ClientOptions {
serviceName: string;
url: string;
username: string;
password: string;
log: ToolingLog;
}
function createClientUrlWithAuth({ serviceName, url, username, password, log }: ClientOptions) {
const clientUrl = new URL(url);
clientUrl.username = username;
clientUrl.password = password;
log.debug(serviceLoadedMsg(`${serviceName}client`));
return clientUrl.toString();
}
export function createEsClient(config: ScoutServerConfig, log: ToolingLog) {
const { username, password } = config.auth;
const elasticsearchUrl = createClientUrlWithAuth({
serviceName: 'Es',
url: config.hosts.elasticsearch,
username,
password,
log,
});
return createEsClientForTesting({
esUrl: elasticsearchUrl,
authOverride: { username, password },
});
}
export function createKbnClient(config: ScoutServerConfig, log: ToolingLog) {
const kibanaUrl = createClientUrlWithAuth({
serviceName: 'Kbn',
url: config.hosts.kibana,
username: config.auth.username,
password: config.auth.password,
log,
});
return new KbnClient({ log, url: kibanaUrl });
}

View file

@ -0,0 +1,29 @@
/*
* 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 path from 'path';
import fs from 'fs';
import { ToolingLog } from '@kbn/tooling-log';
import { ScoutServerConfig } from '../../types';
import { serviceLoadedMsg } from '../../playwright/utils';
export function createScoutConfig(configDir: string, configName: string, log: ToolingLog) {
if (!configDir || !fs.existsSync(configDir)) {
throw new Error(`Directory with servers configuration is missing`);
}
const configPath = path.join(configDir, `${configName}.json`);
log.info(`Reading test servers confiuration from file: ${configPath}`);
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as ScoutServerConfig;
log.debug(serviceLoadedMsg('config'));
return config;
}

View file

@ -0,0 +1,28 @@
/*
* 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 { Client } from '@elastic/elasticsearch';
import { EsArchiver } from '@kbn/es-archiver';
import { REPO_ROOT } from '@kbn/repo-info';
import type { KbnClient } from '@kbn/test';
import type { ToolingLog } from '@kbn/tooling-log';
import { serviceLoadedMsg } from '../../playwright/utils';
export function createEsArchiver(esClient: Client, kbnClient: KbnClient, log: ToolingLog) {
const esArchiver = new EsArchiver({
log,
client: esClient,
kbnClient,
baseDir: REPO_ROOT,
});
log.debug(serviceLoadedMsg('esArchiver'));
return esArchiver;
}

View file

@ -0,0 +1,17 @@
/*
* 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 { createEsClient, createKbnClient } from './clients';
export { createScoutConfig } from './config';
export { createEsArchiver } from './es_archiver';
export { createKbnUrl } from './kibana_url';
export { createSamlSessionManager } from './saml_auth';
export { createLogger } from './logger';
export type { KibanaUrl } from './kibana_url';

View file

@ -0,0 +1,73 @@
/*
* 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 { ToolingLog } from '@kbn/tooling-log';
import { ScoutServerConfig } from '../../types';
import { serviceLoadedMsg } from '../../playwright/utils';
export interface PathOptions {
/**
* Query string parameters
*/
params?: Record<string, string>;
/**
* The hash value of the URL
*/
hash?: string;
}
export class KibanaUrl {
#baseUrl: URL;
constructor(baseUrl: URL) {
this.#baseUrl = baseUrl;
}
/**
* Get an absolute URL based on Kibana's URL
* @param rel relative url, resolved relative to Kibana's url
* @param options optional modifications to apply to the URL
*/
get(rel?: string, options?: PathOptions) {
const url = new URL(rel ?? '/', this.#baseUrl);
if (options?.params) {
for (const [key, value] of Object.entries(options.params)) {
url.searchParams.set(key, value);
}
}
if (options?.hash !== undefined) {
url.hash = options.hash;
}
return url.href;
}
/**
* Get the URL for an app
* @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);
}
toString() {
return this.#baseUrl.href;
}
}
export function createKbnUrl(scoutConfig: ScoutServerConfig, log: ToolingLog) {
const kbnUrl = new KibanaUrl(new URL(scoutConfig.hosts.kibana));
log.debug(serviceLoadedMsg('kbnUrl'));
return kbnUrl;
}

View file

@ -0,0 +1,19 @@
/*
* 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 '@kbn/tooling-log';
import { serviceLoadedMsg } from '../../playwright/utils';
export function createLogger() {
const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout });
log.debug(serviceLoadedMsg('logger'));
return log;
}

View file

@ -0,0 +1,71 @@
/*
* 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 path from 'path';
import { URL } from 'url';
import {
SERVERLESS_ROLES_ROOT_PATH,
STATEFUL_ROLES_ROOT_PATH,
readRolesDescriptorsFromResource,
} from '@kbn/es';
import { REPO_ROOT } from '@kbn/repo-info';
import { HostOptions, SamlSessionManager } from '@kbn/test';
import { ToolingLog } from '@kbn/tooling-log';
import { ScoutServerConfig } from '../../types';
import { Protocol } from '../../playwright/types';
import { serviceLoadedMsg } from '../../playwright/utils';
const getResourceDirPath = (config: ScoutServerConfig) => {
return config.serverless
? path.resolve(SERVERLESS_ROLES_ROOT_PATH, config.projectType!)
: path.resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH);
};
const createKibanaHostOptions = (config: ScoutServerConfig): HostOptions => {
const kibanaUrl = new URL(config.hosts.kibana);
kibanaUrl.username = config.auth.username;
kibanaUrl.password = config.auth.password;
return {
protocol: kibanaUrl.protocol.replace(':', '') as Protocol,
hostname: kibanaUrl.hostname,
port: Number(kibanaUrl.port),
username: kibanaUrl.username,
password: kibanaUrl.password,
};
};
export const createSamlSessionManager = (
config: ScoutServerConfig,
log: ToolingLog
): SamlSessionManager => {
const resourceDirPath = getResourceDirPath(config);
const rolesDefinitionPath = path.resolve(resourceDirPath, 'roles.yml');
const supportedRoleDescriptors = readRolesDescriptorsFromResource(rolesDefinitionPath) as Record<
string,
unknown
>;
const supportedRoles = Object.keys(supportedRoleDescriptors);
const sessionManager = new SamlSessionManager({
hostOptions: createKibanaHostOptions(config),
log,
isCloud: config.isCloud,
supportedRoles: {
roles: supportedRoles,
sourcePath: rolesDefinitionPath,
},
cloudUsersFilePath: config.cloudUsersFilePath,
});
log.debug(serviceLoadedMsg('samlAuth'));
return sessionManager;
};

View file

@ -0,0 +1,20 @@
/*
* 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 '@kbn/tooling-log';
import * as Rx from 'rxjs';
export async function silence(log: ToolingLog, milliseconds: number) {
await Rx.firstValueFrom(
log.getWritten$().pipe(
Rx.startWith(null),
Rx.switchMap(() => Rx.timer(milliseconds))
)
);
}

View file

@ -0,0 +1,138 @@
/*
* 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 { Schema } from 'joi';
import * as Url from 'url';
import Path from 'path';
import { cloneDeepWith, get, has, toPath } from 'lodash';
import { REPO_ROOT } from '@kbn/repo-info';
import { schema } from './schema';
import { ScoutServerConfig } from '../types';
import { formatCurrentDate, getProjectType } from './utils';
const $values = Symbol('values');
export class Config {
private [$values]: Record<string, any>;
constructor(data: Record<string, any>) {
const { error, value } = schema.validate(data, {
abortEarly: false,
});
if (error) {
throw error;
}
this[$values] = value;
}
public has(key: string | string[]) {
function recursiveHasCheck(
remainingPath: string[],
values: Record<string, any>,
childSchema: any
): boolean {
if (!childSchema.$_terms.keys && !childSchema.$_terms.patterns) {
return false;
}
// normalize child and pattern checks so we can iterate the checks in a single loop
const checks: Array<{ test: (k: string) => boolean; schema: Schema }> = [
// match children first, they have priority
...(childSchema.$_terms.keys || []).map((child: { key: string; schema: Schema }) => ({
test: (k: string) => child.key === k,
schema: child.schema,
})),
// match patterns on any key that doesn't match an explicit child
...(childSchema.$_terms.patterns || []).map((pattern: { regex: RegExp; rule: Schema }) => ({
test: (k: string) => pattern.regex.test(k) && has(values, k),
schema: pattern.rule,
})),
];
for (const check of checks) {
if (!check.test(remainingPath[0])) {
continue;
}
if (remainingPath.length > 1) {
return recursiveHasCheck(
remainingPath.slice(1),
get(values, remainingPath[0]),
check.schema
);
}
return true;
}
return false;
}
const path = toPath(key);
if (!path.length) {
return true;
}
return recursiveHasCheck(path, this[$values], schema);
}
public get(key: string | string[], defaultValue?: any) {
if (!this.has(key)) {
throw new Error(`Unknown config key "${key}"`);
}
return cloneDeepWith(get(this[$values], key, defaultValue), (v) => {
if (typeof v === 'function') {
return v;
}
});
}
public getAll() {
return cloneDeepWith(this[$values], (v) => {
if (typeof v === 'function') {
return v;
}
});
}
public getTestServersConfig(): ScoutServerConfig {
return {
serverless: this.get('serverless'),
projectType: this.get('serverless')
? getProjectType(this.get('kbnTestServer.serverArgs'))
: undefined,
isCloud: false,
cloudUsersFilePath: Path.resolve(REPO_ROOT, '.ftr', 'role_users.json'),
hosts: {
kibana: Url.format({
protocol: this.get('servers.kibana.protocol'),
hostname: this.get('servers.kibana.hostname'),
port: this.get('servers.kibana.port'),
}),
elasticsearch: Url.format({
protocol: this.get('servers.elasticsearch.protocol'),
hostname: this.get('servers.elasticsearch.hostname'),
port: this.get('servers.elasticsearch.port'),
}),
},
auth: {
username: this.get('servers.kibana.username'),
password: this.get('servers.kibana.password'),
},
metadata: {
generatedOn: formatCurrentDate(),
config: this.getAll(),
},
};
}
}

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 { resolve } from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
const SECURITY_TEST_PATH = resolve(REPO_ROOT, 'x-pack/test/security_api_integration');
export const SAML_IDP_PLUGIN_PATH = resolve(SECURITY_TEST_PATH, 'plugins/saml_provider');
export const STATEFUL_IDP_METADATA_PATH = resolve(
SECURITY_TEST_PATH,
'packages/helpers/saml/idp_metadata_mock_idp.xml'
);
export const SERVERLESS_IDP_METADATA_PATH = resolve(SAML_IDP_PLUGIN_PATH, 'metadata.xml');
export const JWKS_PATH = resolve(SECURITY_TEST_PATH, 'packages/helpers/oidc/jwks.json');

View file

@ -0,0 +1,26 @@
/*
* 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 path from 'path';
import { CliSupportedServerModes } from '../types';
export const getConfigFilePath = (config: CliSupportedServerModes): string => {
if (config === 'stateful') {
return path.join(__dirname, 'stateful', 'stateful.config.ts');
}
const [mode, type] = config.split('=');
if (mode !== 'serverless' || !type) {
throw new Error(
`Invalid config format: ${config}. Expected "stateful" or "serverless=<type>".`
);
}
return path.join(__dirname, 'serverless', `${type}.serverless.config.ts`);
};

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 { loadConfig } from './loader/config_load';
export { getConfigFilePath } from './get_config_file';
export { loadServersConfig } from './utils';
export type { Config } from './config';

View file

@ -0,0 +1,27 @@
/*
* 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 path from 'path';
import { ToolingLog } from '@kbn/tooling-log';
import { Config } from '../config';
export const loadConfig = async (configPath: string, log: ToolingLog): Promise<Config> => {
try {
const absolutePath = path.resolve(configPath);
const configModule = await import(absolutePath);
if (configModule.servers) {
return new Config(configModule.servers);
} else {
throw new Error(`No 'servers' found in the config file at path: ${absolutePath}`);
}
} catch (error) {
throw new Error(`Failed to load config from ${configPath}: ${error.message}`);
}
};

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", 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 { schema } from './schema';

View file

@ -0,0 +1,139 @@
/*
* 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 Joi from 'joi';
const maybeRequireKeys = (keys: string[], schemas: Record<string, Joi.Schema>) => {
if (!keys.length) {
return schemas;
}
const withRequires: Record<string, Joi.Schema> = {};
for (const [key, schema] of Object.entries(schemas)) {
withRequires[key] = keys.includes(key) ? schema.required() : schema;
}
return withRequires;
};
const urlPartsSchema = ({ requiredKeys }: { requiredKeys?: string[] } = {}) =>
Joi.object()
.keys(
maybeRequireKeys(requiredKeys ?? [], {
protocol: Joi.string().valid('http', 'https').default('http'),
hostname: Joi.string().hostname().default('localhost'),
port: Joi.number(),
auth: Joi.string().regex(/^[^:]+:.+$/, 'username and password separated by a colon'),
username: Joi.string(),
password: Joi.string(),
pathname: Joi.string().regex(/^\//, 'start with a /'),
hash: Joi.string().regex(/^\//, 'start with a /'),
certificateAuthorities: Joi.array().items(Joi.binary()).optional(),
})
)
.default();
const requiredWhenEnabled = (schema: Joi.Schema) => {
return Joi.when('enabled', {
is: true,
then: schema.required(),
otherwise: schema.optional(),
});
};
const dockerServerSchema = () =>
Joi.object()
.keys({
enabled: Joi.boolean().required(),
image: requiredWhenEnabled(Joi.string()),
port: requiredWhenEnabled(Joi.number()),
portInContainer: requiredWhenEnabled(Joi.number()),
waitForLogLine: Joi.alternatives(Joi.object().instance(RegExp), Joi.string()).optional(),
waitForLogLineTimeoutMs: Joi.number().integer().optional(),
waitFor: Joi.func().optional(),
args: Joi.array().items(Joi.string()).optional(),
})
.default();
export const schema = Joi.object()
.keys({
serverless: Joi.boolean().default(false),
servers: Joi.object()
.keys({
kibana: urlPartsSchema(),
elasticsearch: urlPartsSchema({
requiredKeys: ['port'],
}),
fleetserver: urlPartsSchema(),
})
.default(),
esTestCluster: Joi.object()
.keys({
license: Joi.valid('basic', 'trial', 'gold').default('basic'),
from: Joi.string().default('snapshot'),
serverArgs: Joi.array().items(Joi.string()).default([]),
esJavaOpts: Joi.string(),
dataArchive: Joi.string(),
ssl: Joi.boolean().default(false),
ccs: Joi.object().keys({
remoteClusterUrl: Joi.string().uri({
scheme: /https?/,
}),
}),
files: Joi.array().items(Joi.string()),
})
.default(),
esServerlessOptions: Joi.object()
.keys({
host: Joi.string().ip(),
resources: Joi.array().items(Joi.string()).default([]),
})
.default(),
kbnTestServer: Joi.object()
.keys({
buildArgs: Joi.array(),
sourceArgs: Joi.array(),
serverArgs: Joi.array(),
installDir: Joi.string(),
useDedicatedTaskRunner: Joi.boolean().default(false),
/** Options for how FTR should execute and interact with Kibana */
runOptions: Joi.object()
.keys({
/**
* Log message to wait for before initiating tests, defaults to waiting for Kibana status to be `available`.
* Note that this log message must not be filtered out by the current logging config, for example by the
* log level. If needed, you can adjust the logging level via `kbnTestServer.serverArgs`.
*/
wait: Joi.object()
.regex()
.default(/Kibana is now available/),
/**
* Does this test config only work when run against source?
*/
alwaysUseSource: Joi.boolean().default(false),
})
.default(),
env: Joi.object().unknown().default(),
delayShutdown: Joi.number(),
})
.default(),
// settings for the kibanaServer.uiSettings module
uiSettings: Joi.object()
.keys({
defaults: Joi.object().unknown(true),
})
.default(),
dockerServers: Joi.object().pattern(Joi.string(), dockerServerSchema()).default(),
})
.default();

View file

@ -0,0 +1,26 @@
/*
* 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 { ScoutLoaderConfig } from '../../types';
import { defaultConfig } from './serverless.base.config';
export const servers: ScoutLoaderConfig = {
...defaultConfig,
esTestCluster: {
...defaultConfig.esTestCluster,
serverArgs: [...defaultConfig.esTestCluster.serverArgs],
},
kbnTestServer: {
serverArgs: [
...defaultConfig.kbnTestServer.serverArgs,
'--serverless=es',
'--coreApp.allowDynamicConfigOverrides=true',
],
},
};

View file

@ -0,0 +1,32 @@
/*
* 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 { defaultConfig } from './serverless.base.config';
import { ScoutLoaderConfig } from '../../types';
export const servers: ScoutLoaderConfig = {
...defaultConfig,
esTestCluster: {
...defaultConfig.esTestCluster,
serverArgs: [
...defaultConfig.esTestCluster.serverArgs,
'xpack.apm_data.enabled=true',
// for ML, data frame analytics are not part of this project type
'xpack.ml.dfa.enabled=false',
],
},
kbnTestServer: {
serverArgs: [
...defaultConfig.kbnTestServer.serverArgs,
'--serverless=oblt',
'--coreApp.allowDynamicConfigOverrides=true',
'--xpack.uptime.service.manifestUrl=mockDevUrl',
],
},
};

View file

@ -0,0 +1,2 @@
package_paths:
- /packages/package-storage

View file

@ -0,0 +1,30 @@
/*
* 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 { ScoutLoaderConfig } from '../../types';
import { defaultConfig } from './serverless.base.config';
export const servers: ScoutLoaderConfig = {
...defaultConfig,
esTestCluster: {
...defaultConfig.esTestCluster,
serverArgs: [
...defaultConfig.esTestCluster.serverArgs,
'xpack.security.authc.api_key.cache.max_keys=70000',
],
},
kbnTestServer: {
serverArgs: [
...defaultConfig.kbnTestServer.serverArgs,
'--serverless=security',
'--coreApp.allowDynamicConfigOverrides=true',
`--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify(['Fleet-Metrics-Task'])}`,
],
},
};

View file

@ -0,0 +1,157 @@
/*
* 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 { resolve, join } from 'path';
import { format as formatUrl } from 'url';
import Fs from 'fs';
import { CA_CERT_PATH, kibanaDevServiceAccount } from '@kbn/dev-utils';
import { defineDockerServersConfig, getDockerFileMountPath } from '@kbn/test';
import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils';
import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base';
import { REPO_ROOT } from '@kbn/repo-info';
import { ScoutLoaderConfig } from '../../types';
import { SAML_IDP_PLUGIN_PATH, SERVERLESS_IDP_METADATA_PATH, JWKS_PATH } from '../constants';
const packageRegistryConfig = join(__dirname, './package_registry_config.yml');
const dockerArgs: string[] = ['-v', `${packageRegistryConfig}:/package-registry/config.yml`];
/**
* This is used by CI to set the docker registry port
* you can also define this environment variable locally when running tests which
* will spin up a local docker package registry locally for you
* if this is defined it takes precedence over the `packageRegistryOverride` variable
*/
const dockerRegistryPort: string | undefined = process.env.FLEET_PACKAGE_REGISTRY_PORT;
const servers = {
elasticsearch: {
protocol: 'https',
hostname: 'localhost',
port: 9220,
username: 'elastic_serverless',
password: 'changeme',
certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)],
},
kibana: {
protocol: 'http',
hostname: 'localhost',
port: 5620,
username: 'elastic_serverless',
password: 'changeme',
},
};
export const defaultConfig: ScoutLoaderConfig = {
serverless: true,
servers,
dockerServers: defineDockerServersConfig({
registry: {
enabled: !!dockerRegistryPort,
image: dockerImage,
portInContainer: 8080,
port: dockerRegistryPort,
args: dockerArgs,
waitForLogLine: 'package manifests loaded',
waitForLogLineTimeoutMs: 60 * 2 * 1000, // 2 minutes
},
}),
esTestCluster: {
from: 'serverless',
files: [SERVERLESS_IDP_METADATA_PATH, JWKS_PATH],
serverArgs: [
'xpack.security.authc.realms.file.file1.order=-100',
`xpack.security.authc.realms.native.native1.enabled=false`,
`xpack.security.authc.realms.native.native1.order=-97`,
'xpack.security.authc.realms.jwt.jwt1.allowed_audiences=elasticsearch',
`xpack.security.authc.realms.jwt.jwt1.allowed_issuer=https://kibana.elastic.co/jwt/`,
`xpack.security.authc.realms.jwt.jwt1.allowed_signature_algorithms=[RS256]`,
`xpack.security.authc.realms.jwt.jwt1.allowed_subjects=elastic-agent`,
`xpack.security.authc.realms.jwt.jwt1.claims.principal=sub`,
'xpack.security.authc.realms.jwt.jwt1.client_authentication.type=shared_secret',
'xpack.security.authc.realms.jwt.jwt1.order=-98',
`xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path=${getDockerFileMountPath(JWKS_PATH)}`,
`xpack.security.authc.realms.jwt.jwt1.token_type=access_token`,
],
ssl: true, // SSL is required for SAML realm
},
kbnTestServer: {
buildArgs: [],
env: {
KBN_PATH_CONF: resolve(REPO_ROOT, 'config'),
},
sourceArgs: ['--no-base-path', '--env.name=development'],
serverArgs: [
`--server.restrictInternalApis=true`,
`--server.port=${servers.kibana.port}`,
'--status.allowAnonymous=true',
`--migrations.zdt.runOnRoles=${JSON.stringify(['ui'])}`,
// We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should
// either include `kibanaServerTestUser` credentials, or credentials provided by the test
// user, or none at all in case anonymous access is used.
`--elasticsearch.hosts=${formatUrl(
Object.fromEntries(
Object.entries(servers.elasticsearch).filter(([key]) => key.toLowerCase() !== 'auth')
)
)}`,
`--elasticsearch.serviceAccountToken=${kibanaDevServiceAccount.token}`,
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
'--telemetry.sendUsageTo=staging',
`--logging.appenders.deprecation=${JSON.stringify({
type: 'console',
layout: {
type: 'json',
},
})}`,
`--logging.loggers=${JSON.stringify([
{
name: 'elasticsearch.deprecation',
level: 'all',
appenders: ['deprecation'],
},
])}`,
// Add meta info to the logs so FTR logs are more actionable
`--logging.appenders.default=${JSON.stringify({
type: 'console',
layout: {
type: 'pattern',
pattern: '[%date][%level][%logger] %message %meta',
},
})}`,
`--logging.appenders.console=${JSON.stringify({
type: 'console',
layout: {
type: 'pattern',
pattern: '[%date][%level][%logger] %message %meta',
},
})}`,
// This ensures that we register the Security SAML API endpoints.
// In the real world the SAML config is injected by control plane.
`--plugin-path=${SAML_IDP_PLUGIN_PATH}`,
'--xpack.cloud.id=ftr_fake_cloud_id',
// Ensure that SAML is used as the default authentication method whenever a user navigates to Kibana. In other
// words, Kibana should attempt to authenticate the user using the provider with the lowest order if the Login
// Selector is disabled (which is how Serverless Kibana is configured). By declaring `cloud-basic` with a higher
// order, we indicate that basic authentication can still be used, but only if explicitly requested when the
// user navigates to `/login` page directly and enters username and password in the login form.
'--xpack.security.authc.selector.enabled=false',
`--xpack.security.authc.providers=${JSON.stringify({
saml: { 'cloud-saml-kibana': { order: 0, realm: MOCK_IDP_REALM_NAME } },
basic: { 'cloud-basic': { order: 1 } },
})}`,
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
`--server.publicBaseUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`,
// configure security reponse header report-to settings to mimic MKI configuration
`--csp.report_to=${JSON.stringify(['violations-endpoint'])}`,
`--permissionsPolicy.report_to=${JSON.stringify(['violations-endpoint'])}`,
],
},
};

View file

@ -0,0 +1,204 @@
/*
* 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 { join } from 'path';
import { format as formatUrl } from 'url';
import {
MOCK_IDP_ENTITY_ID,
MOCK_IDP_ATTRIBUTE_PRINCIPAL,
MOCK_IDP_ATTRIBUTE_ROLES,
MOCK_IDP_ATTRIBUTE_EMAIL,
MOCK_IDP_ATTRIBUTE_NAME,
} from '@kbn/mock-idp-utils';
import { defineDockerServersConfig } from '@kbn/test';
import path from 'path';
import { MOCK_IDP_REALM_NAME } from '@kbn/mock-idp-utils';
import { dockerImage } from '@kbn/test-suites-xpack/fleet_api_integration/config.base';
import { REPO_ROOT } from '@kbn/repo-info';
import { STATEFUL_ROLES_ROOT_PATH } from '@kbn/es';
import type { ScoutLoaderConfig } from '../../types';
import { SAML_IDP_PLUGIN_PATH, STATEFUL_IDP_METADATA_PATH } from '../constants';
const packageRegistryConfig = join(__dirname, './package_registry_config.yml');
const dockerArgs: string[] = ['-v', `${packageRegistryConfig}:/package-registry/config.yml`];
/**
* This is used by CI to set the docker registry port
* you can also define this environment variable locally when running tests which
* will spin up a local docker package registry locally for you
* if this is defined it takes precedence over the `packageRegistryOverride` variable
*/
const dockerRegistryPort: string | undefined = process.env.FLEET_PACKAGE_REGISTRY_PORT;
// if config is executed on CI or locally
const isRunOnCI = process.env.CI;
const servers = {
elasticsearch: {
protocol: 'http',
hostname: 'localhost',
port: 9220,
username: 'kibana_system',
password: 'changeme',
},
kibana: {
protocol: 'http',
hostname: 'localhost',
port: 5620,
username: 'elastic',
password: 'changeme',
},
};
const kbnUrl = `${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`;
export const defaultConfig: ScoutLoaderConfig = {
servers,
dockerServers: defineDockerServersConfig({
registry: {
enabled: !!dockerRegistryPort,
image: dockerImage,
portInContainer: 8080,
port: dockerRegistryPort,
args: dockerArgs,
waitForLogLine: 'package manifests loaded',
waitForLogLineTimeoutMs: 60 * 2 * 1000, // 2 minutes
},
}),
esTestCluster: {
from: 'snapshot',
license: 'trial',
files: [
// Passing the roles that are equivalent to the ones we have in serverless
path.resolve(REPO_ROOT, STATEFUL_ROLES_ROOT_PATH, 'roles.yml'),
],
serverArgs: [
'path.repo=/tmp/',
'path.repo=/tmp/repo,/tmp/repo_1,/tmp/repo_2,/tmp/cloud-snapshots/',
'node.attr.name=apiIntegrationTestNode',
'xpack.security.authc.api_key.enabled=true',
'xpack.security.authc.token.enabled=true',
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.order=0`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.metadata.path=${STATEFUL_IDP_METADATA_PATH}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.idp.entity_id=${MOCK_IDP_ENTITY_ID}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.entity_id=${kbnUrl}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.acs=${kbnUrl}/api/security/saml/callback`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.sp.logout=${kbnUrl}/logout`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.principal=${MOCK_IDP_ATTRIBUTE_PRINCIPAL}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.groups=${MOCK_IDP_ATTRIBUTE_ROLES}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.name=${MOCK_IDP_ATTRIBUTE_NAME}`,
`xpack.security.authc.realms.saml.${MOCK_IDP_REALM_NAME}.attributes.mail=${MOCK_IDP_ATTRIBUTE_EMAIL}`,
],
ssl: false,
},
kbnTestServer: {
buildArgs: [],
env: {},
sourceArgs: ['--no-base-path', '--env.name=development'],
serverArgs: [
`--server.port=${servers.kibana.port}`,
'--status.allowAnonymous=true',
// We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should
// either include `kibanaServerTestUser` credentials, or credentials provided by the test
// user, or none at all in case anonymous access is used.
`--elasticsearch.hosts=${formatUrl(
Object.fromEntries(
Object.entries(servers.elasticsearch).filter(([key]) => key.toLowerCase() !== 'auth')
)
)}`,
`--elasticsearch.username=${servers.elasticsearch.username}`,
`--elasticsearch.password=${servers.elasticsearch.password}`,
// Needed for async search functional tests to introduce a delay
`--data.search.aggs.shardDelay.enabled=true`,
`--data.query.timefilter.minRefreshInterval=1000`,
`--security.showInsecureClusterWarning=false`,
'--telemetry.banner=false',
'--telemetry.optIn=false',
// These are *very* important to have them pointing to staging
'--telemetry.sendUsageTo=staging',
`--server.maxPayload=1679958`,
// newsfeed mock service
`--plugin-path=${path.join(REPO_ROOT, 'test', 'common', 'plugins', 'newsfeed')}`,
// otel mock service
`--plugin-path=${path.join(REPO_ROOT, 'test', 'common', 'plugins', 'otel_metrics')}`,
`--newsfeed.service.urlRoot=${kbnUrl}`,
`--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/v{VERSION}.json`,
`--logging.appenders.deprecation=${JSON.stringify({
type: 'console',
layout: {
type: 'json',
},
})}`,
`--logging.loggers=${JSON.stringify([
{
name: 'elasticsearch.deprecation',
level: 'all',
appenders: ['deprecation'],
},
])}`,
// Add meta info to the logs so FTR logs are more actionable
`--logging.appenders.default=${JSON.stringify({
type: 'console',
layout: {
type: 'pattern',
pattern: '[%date][%level][%logger] %message %meta',
},
})}`,
`--logging.appenders.console=${JSON.stringify({
type: 'console',
layout: {
type: 'pattern',
pattern: '[%date][%level][%logger] %message %meta',
},
})}`,
// x-pack/test/functional/config.base.js
'--status.allowAnonymous=true',
'--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d',
'--xpack.maps.showMapsInspectorAdapter=true',
'--xpack.maps.preserveDrawingBuffer=true',
'--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions
'--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"',
'--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true',
'--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects,
'--savedObjects.allowHttpApiAccess=false', // override default to not allow hiddenFromHttpApis saved objects access to the http APIs see https://github.com/elastic/dev/issues/2200
// explicitly disable internal API restriction. See https://github.com/elastic/kibana/issues/163654
'--server.restrictInternalApis=false',
// disable fleet task that writes to metrics.fleet_server.* data streams, impacting functional tests
`--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify(['Fleet-Metrics-Task'])}`,
// x-pack/test/api_integration/config.ts
'--xpack.security.session.idleTimeout=3600000', // 1 hour
'--telemetry.optIn=true',
'--xpack.fleet.agents.pollingRequestTimeout=5000', // 5 seconds
'--xpack.ruleRegistry.write.enabled=true',
'--xpack.ruleRegistry.write.enabled=true',
'--xpack.ruleRegistry.write.cache.enabled=false',
'--monitoring_collection.opentelemetry.metrics.prometheus.enabled=true',
// SAML configuration
...(isRunOnCI ? [] : ['--mock_idp_plugin.enabled=true']),
// This ensures that we register the Security SAML API endpoints.
// In the real world the SAML config is injected by control plane.
`--plugin-path=${SAML_IDP_PLUGIN_PATH}`,
'--xpack.cloud.id=ftr_fake_cloud_id',
// Ensure that SAML is used as the default authentication method whenever a user navigates to Kibana. In other
// words, Kibana should attempt to authenticate the user using the provider with the lowest order if the Login
// Selector is disabled (replicating Serverless configuration). By declaring `cloud-basic` with a higher
// order, we indicate that basic authentication can still be used, but only if explicitly requested when the
// user navigates to `/login` page directly and enters username and password in the login form.
'--xpack.security.authc.selector.enabled=false',
`--xpack.security.authc.providers=${JSON.stringify({
saml: { 'cloud-saml-kibana': { order: 0, realm: MOCK_IDP_REALM_NAME } },
basic: { 'cloud-basic': { order: 1 } },
})}`,
`--server.publicBaseUrl=${kbnUrl}`,
],
},
};

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".
*/
import { ScoutLoaderConfig } from '../../types';
import { defaultConfig } from './base.config';
export const servers: ScoutLoaderConfig = defaultConfig;

View file

@ -0,0 +1,68 @@
/*
* 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 * as Fs from 'fs';
import getopts from 'getopts';
import path from 'path';
import { ToolingLog } from '@kbn/tooling-log';
import { ServerlessProjectType } from '@kbn/es';
import { REPO_ROOT } from '@kbn/repo-info';
import { CliSupportedServerModes, ScoutServerConfig } from '../types';
import { getConfigFilePath } from './get_config_file';
import { loadConfig } from './loader/config_load';
export const formatCurrentDate = () => {
const now = new Date();
const format = (num: number, length: number) => String(num).padStart(length, '0');
return (
`${format(now.getDate(), 2)}/${format(now.getMonth() + 1, 2)}/${now.getFullYear()} ` +
`${format(now.getHours(), 2)}:${format(now.getMinutes(), 2)}:${format(now.getSeconds(), 2)}.` +
`${format(now.getMilliseconds(), 3)}`
);
};
const saveTestServersConfigOnDisk = (testServersConfig: ScoutServerConfig, log: ToolingLog) => {
const configDirPath = path.resolve(REPO_ROOT, '.scout', 'servers');
const configFilePath = path.join(configDirPath, `local.json`);
try {
const jsonData = JSON.stringify(testServersConfig, null, 2);
if (!Fs.existsSync(configDirPath)) {
log.debug(`scout: creating configuration directory: ${configDirPath}`);
Fs.mkdirSync(configDirPath, { recursive: true });
}
Fs.writeFileSync(configFilePath, jsonData, 'utf-8');
log.info(`scout: Test server configuration saved at ${configFilePath}`);
} catch (error) {
log.error(`scout: Failed to save test server configuration - ${error.message}`);
throw new Error(`Failed to save test server configuration at ${configFilePath}`);
}
};
export async function loadServersConfig(mode: CliSupportedServerModes, log: ToolingLog) {
// get path to one of the predefined config files
const configPath = getConfigFilePath(mode);
// load config that is compatible with kbn-test input format
const config = await loadConfig(configPath, log);
// construct config for Playwright Test
const scoutServerConfig = config.getTestServersConfig();
// save test config to the file
saveTestServersConfigOnDisk(scoutServerConfig, log);
return config;
}
export const getProjectType = (kbnServerArgs: string[]) => {
const options = getopts(kbnServerArgs);
return options.serverless as ServerlessProjectType;
};

View file

@ -0,0 +1,75 @@
/*
* 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 { defineConfig, PlaywrightTestConfig, devices } from '@playwright/test';
import * as Path from 'path';
import { REPO_ROOT } from '@kbn/repo-info';
import { ScoutPlaywrightOptions, ScoutTestOptions, VALID_CONFIG_MARKER } from '../types';
export function createPlaywrightConfig(options: ScoutPlaywrightOptions): PlaywrightTestConfig {
return defineConfig<ScoutTestOptions>({
testDir: options.testDir,
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: options.workers ?? 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
['html', { outputFolder: './output/reports', open: 'never' }], // HTML report configuration
['json', { outputFile: './output/reports/test-results.json' }], // JSON report
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
serversConfigDir: Path.resolve(REPO_ROOT, '.scout', 'servers'),
[VALID_CONFIG_MARKER]: true,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
screenshot: 'only-on-failure',
// video: 'retain-on-failure',
// storageState: './output/reports/state.json', // Store session state (like cookies)
},
// Timeout for each test, includes test, hooks and fixtures
timeout: 60000,
// Timeout for each assertion
expect: {
timeout: 10000,
},
outputDir: './output/test-artifacts', // For other test artifacts (screenshots, videos, traces)
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});
}

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".
*/
import { test } from '@playwright/test';
// Export `expect` to avoid importing from Playwright directly
export const expect = test.expect;

View file

@ -0,0 +1,25 @@
/*
* 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 { scoutWorkerFixtures } from './worker';
import { scoutTestFixtures } from './test';
export const scoutCoreFixtures = mergeTests(scoutWorkerFixtures, scoutTestFixtures);
export type {
ScoutTestFixtures,
ScoutWorkerFixtures,
ScoutPage,
Client,
KbnClient,
KibanaUrl,
ToolingLog,
} from './types';

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 { 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>;
export const browserAuthFixture = base.extend<{ browserAuth: LoginFixture }, ScoutWorkerFixtures>({
browserAuth: async ({ log, context, samlAuth, config }, 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 cookie = await samlAuth.getInteractiveUserSessionCookieWithRoleScope(role);
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'));
await use({ loginAsAdmin, loginAsViewer, loginAsPrivilegedUser });
},
});

View file

@ -0,0 +1,19 @@
/*
* 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 { browserAuthFixture } from './browser_auth';
import { scoutPageFixture } from './page';
import { pageObjectsFixture } from './page_objects';
export const scoutTestFixtures = mergeTests(
browserAuthFixture,
scoutPageFixture,
pageObjectsFixture
);

View file

@ -0,0 +1,83 @@
/*
* 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 { subj } from '@kbn/test-subj-selector';
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) {
const methods: Array<keyof Page> = [
'check',
'click',
'dblclick',
'fill',
'focus',
'getAttribute',
'hover',
'isEnabled',
'innerText',
'isChecked',
'isHidden',
'locator',
];
const extendedMethods: Partial<Record<keyof Page, Function>> = {};
for (const method of methods) {
extendedMethods[method] = (...args: any[]) => {
const selector = args[0];
const testSubjSelector = subj(selector);
return (page[method] as Function)(testSubjSelector, ...args.slice(1));
};
}
return extendedMethods as Record<keyof Page, any>;
}
/**
* Extends the 'page' fixture with Kibana-specific functionality
*
* 1. Allow calling methods with simplified 'data-test-subj' selectors.
* Instead of manually constructing 'data-test-subj' selectors, this extension provides a `testSubj` object on the page
* Supported methods include `click`, `check`, `fill`, and others that interact with `data-test-subj`.
*
* Example Usage:
*
* ```typescript
* // Without `testSubj` extension:
* await page.locator('[data-test-subj="foo"][data-test-subj="bar"]').click();
*
* // With `testSubj` extension:
* await page.testSubj.click('foo & bar');
* ```
*
* 2. Navigate to Kibana apps by using 'kbnUrl' fixture
*
* Example Usage:
*
* ```typescript
* // Navigate to '/app/discover'
* await page.gotoApp('discover);
* ```
*/
export const scoutPageFixture = base.extend<{ page: ScoutPage; kbnUrl: KibanaUrl }>({
page: async ({ page, kbnUrl }, use) => {
// Extend page with '@kbn/test-subj-selector' support
page.testSubj = extendPageWithTestSubject(page);
// Method to navigate to specific Kibana apps
page.gotoApp = (appName: string) => page.goto(kbnUrl.app(appName));
await use(page);
},
});

View file

@ -0,0 +1,20 @@
/*
* 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 { ScoutTestFixtures, ScoutWorkerFixtures } from '../types';
import { createCorePageObjects } from '../../page_objects';
export const pageObjectsFixture = base.extend<ScoutTestFixtures, ScoutWorkerFixtures>({
pageObjects: async ({ page }, use) => {
const corePageObjects = createCorePageObjects(page);
await use(corePageObjects);
},
});

View file

@ -0,0 +1,11 @@
/*
* 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 * from './test_scope';
export * from './worker_scope';

View file

@ -0,0 +1,67 @@
/*
* 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 } from 'playwright/test';
import { PageObjects } from '../../page_objects';
export interface ScoutTestFixtures {
browserAuth: LoginFixture;
page: ScoutPage;
pageObjects: PageObjects;
}
export interface LoginFixture {
loginAsViewer: () => Promise<void>;
loginAsAdmin: () => Promise<void>;
loginAsPrivilegedUser: () => Promise<void>;
}
export type ScoutPage = Page & {
gotoApp: (appName: string, options?: Parameters<Page['goto']>[1]) => ReturnType<Page['goto']>;
testSubj: {
check: (selector: string, options?: Parameters<Page['check']>[1]) => ReturnType<Page['check']>;
click: (selector: string, options?: Parameters<Page['click']>[1]) => ReturnType<Page['click']>;
dblclick: (
selector: string,
options?: Parameters<Page['dblclick']>[1]
) => ReturnType<Page['dblclick']>;
fill: (
selector: string,
value: string,
options?: Parameters<Page['fill']>[2]
) => ReturnType<Page['fill']>;
focus: (selector: string, options?: Parameters<Page['focus']>[1]) => ReturnType<Page['focus']>;
getAttribute: (
selector: string,
name: string,
options?: Parameters<Page['getAttribute']>[2]
) => ReturnType<Page['getAttribute']>;
hover: (selector: string, options?: Parameters<Page['hover']>[1]) => ReturnType<Page['hover']>;
innerText: (
selector: string,
options?: Parameters<Page['innerText']>[1]
) => ReturnType<Page['innerText']>;
isEnabled: (
selector: string,
options?: Parameters<Page['isEnabled']>[1]
) => ReturnType<Page['isEnabled']>;
isChecked: (
selector: string,
options?: Parameters<Page['isChecked']>[1]
) => ReturnType<Page['isChecked']>;
isHidden: (
selector: string,
options?: Parameters<Page['isHidden']>[1]
) => ReturnType<Page['isHidden']>;
locator: (
selector: string,
options?: Parameters<Page['locator']>[1]
) => ReturnType<Page['locator']>;
};
};

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 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 { ScoutServerConfig } from '../../../types';
import { KibanaUrl } from '../../../common/services/kibana_url';
interface EsArchiverFixture {
loadIfNeeded: (
name: string,
performance?: LoadActionPerfOptions | undefined
) => Promise<Record<string, IndexStats>>;
}
export interface ScoutWorkerFixtures {
log: ToolingLog;
config: ScoutServerConfig;
kbnUrl: KibanaUrl;
esClient: Client;
kbnClient: KbnClient;
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

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

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 { mergeTests } from 'playwright/test';
import { scoutCoreFixtures } from './fixtures';
// Scout core fixtures: worker & test scope
export const test = mergeTests(scoutCoreFixtures);
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, ScoutPage } from './fixtures';

View file

@ -0,0 +1,41 @@
/*
* 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 '..';
export class DatePicker {
constructor(private readonly page: ScoutPage) {}
async setAbsoluteRange({ from, to }: { from: string; to: string }) {
await this.page.testSubj.click('superDatePickerShowDatesButton');
// we start with end date
await this.page.testSubj.click('superDatePickerendDatePopoverButton');
await this.page.testSubj.click('superDatePickerAbsoluteTab');
const inputFrom = this.page.testSubj.locator('superDatePickerAbsoluteDateInput');
await inputFrom.clear();
await inputFrom.fill(to);
await this.page.testSubj.click('parseAbsoluteDateFormat');
await this.page.testSubj.click('superDatePickerendDatePopoverButton');
// and later change start date
await this.page.testSubj.click('superDatePickerstartDatePopoverButton');
await this.page.testSubj.click('superDatePickerAbsoluteTab');
const inputTo = this.page.testSubj.locator('superDatePickerAbsoluteDateInput');
await inputTo.clear();
await inputTo.fill(from);
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 this.page.testSubj.click('querySubmitButton');
}
}

View file

@ -0,0 +1,18 @@
/*
* 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';
export class DiscoverApp {
constructor(private readonly page: ScoutPage) {}
async goto() {
await this.page.gotoApp('discover');
}
}

View file

@ -0,0 +1,32 @@
/*
* 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 { DatePicker } from './date_picker';
import { DiscoverApp } from './discover_app';
import { createLazyPageObject } from './utils';
export interface PageObjects {
datePicker: DatePicker;
discover: DiscoverApp;
}
/**
* Creates a set of core page objects, each lazily instantiated on first access.
*
* @param page - `ScoutPage` instance used for initializing page objects.
* @returns An object containing lazy-loaded core page objects.
*/
export function createCorePageObjects(page: ScoutPage): PageObjects {
return {
datePicker: createLazyPageObject(DatePicker, page),
discover: createLazyPageObject(DiscoverApp, page),
// Add new page objects here
};
}

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 { ScoutPage } from '../../fixtures/types';
/**
* Creates a lazily instantiated proxy for a Page Object class, deferring the creation of the instance until
* a property or method is accessed. It helps avoiding instantiation of page objects that may not be used
* in certain test scenarios.
*
* @param PageObjectClass - The page object class to be instantiated lazily.
* @param scoutPage - ScoutPage instance, that extendes the Playwright `page` fixture and passed to the page object class constructor.
* @param constructorArgs - Additional arguments to be passed to the page object class constructor.
* @returns A proxy object that behaves like an instance of the page object class, instantiating it on demand.
*/
export function createLazyPageObject<T extends object>(
PageObjectClass: new (page: ScoutPage, ...args: any[]) => T,
scoutPage: ScoutPage,
...constructorArgs: any[]
): T {
let instance: T | null = null;
return new Proxy({} as T, {
get(_, prop: string | symbol) {
if (!instance) {
instance = new PageObjectClass(scoutPage, ...constructorArgs);
}
if (typeof prop === 'symbol' || !(prop in instance)) {
return undefined;
}
return instance[prop as keyof T];
},
});
}

View file

@ -0,0 +1,46 @@
/*
* 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 * as Fs from 'fs';
import { REPO_ROOT } from '@kbn/repo-info';
import { PlaywrightTestConfig } from 'playwright/test';
import path from 'path';
import { createFlagError } from '@kbn/dev-cli-errors';
import { ScoutTestOptions, VALID_CONFIG_MARKER } from '../types';
export async function validatePlaywrightConfig(configPath: string) {
const fullPath = path.resolve(REPO_ROOT, configPath);
// Check if the path exists and has a .ts extension
if (!configPath || !Fs.existsSync(fullPath) || !configPath.endsWith('.ts')) {
throw createFlagError(
`Path to a valid TypeScript config file is required: --config <relative path to .ts file>`
);
}
// Dynamically import the file to check for a default export
const configModule = await import(fullPath);
const config = configModule.default as PlaywrightTestConfig<ScoutTestOptions>;
// Check if the config's 'use' property has the valid marker
if (!config?.use?.[VALID_CONFIG_MARKER]) {
throw createFlagError(
`The config file at "${configPath}" must be created with "createPlaywrightConfig" from '@kbn/scout' package:\n
export default createPlaywrightConfig({
testDir: './tests',
});`
);
}
if (!config.testDir) {
throw createFlagError(
`The config file at "${configPath}" must export a valid Playwright configuration with "testDir" property.`
);
}
}

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 { FlagOptions, FlagsReader } from '@kbn/dev-cli-runner';
import { createFlagError } from '@kbn/dev-cli-errors';
import { SERVER_FLAG_OPTIONS, parseServerFlags } from '../../servers';
import { CliSupportedServerModes } from '../../types';
import { validatePlaywrightConfig } from './config_validator';
export interface RunTestsOptions {
configPath: string;
headed: boolean;
mode: CliSupportedServerModes;
esFrom: 'serverless' | 'source' | 'snapshot' | undefined;
installDir: string | undefined;
logsDir: string | undefined;
}
export const TEST_FLAG_OPTIONS: FlagOptions = {
...SERVER_FLAG_OPTIONS,
boolean: [...(SERVER_FLAG_OPTIONS.boolean || []), 'headed'],
string: [...(SERVER_FLAG_OPTIONS.string || []), 'config'],
default: { headed: false },
help: `${SERVER_FLAG_OPTIONS.help}
--config Playwright config file path
--headed Run Playwright with browser head
`,
};
export async function parseTestFlags(flags: FlagsReader) {
const options = parseServerFlags(flags);
const configPath = flags.string('config');
const headed = flags.boolean('headed');
if (!configPath) {
throw createFlagError(`Path to playwright config is required: --config <file path>`);
}
await validatePlaywrightConfig(configPath);
return {
...options,
configPath,
headed,
};
}

View file

@ -0,0 +1,12 @@
/*
* 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 { runTests } from './run_tests';
export { parseTestFlags, TEST_FLAG_OPTIONS } from './flags';
export type { RunTestsOptions } from './flags';

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 { resolve } from 'path';
import { ToolingLog } from '@kbn/tooling-log';
import { withProcRunner } from '@kbn/dev-proc-runner';
import { getTimeReporter } from '@kbn/ci-stats-reporter';
import { REPO_ROOT } from '@kbn/repo-info';
import { runElasticsearch, runKibanaServer } from '../../servers';
import { loadServersConfig } from '../../config';
import { silence } from '../../common';
import { RunTestsOptions } from './flags';
import { getExtraKbnOpts } from '../../servers/run_kibana_server';
export async function runTests(log: ToolingLog, options: RunTestsOptions) {
const runStartTime = Date.now();
const reportTime = getTimeReporter(log, 'scripts/scout_test');
const config = await loadServersConfig(options.mode, log);
const playwrightConfigPath = options.configPath;
await withProcRunner(log, async (procs) => {
const abortCtrl = new AbortController();
const onEarlyExit = (msg: string) => {
log.error(msg);
abortCtrl.abort();
};
let shutdownEs;
try {
shutdownEs = await runElasticsearch({
onEarlyExit,
config,
log,
esFrom: options.esFrom,
logsDir: options.logsDir,
});
await runKibanaServer({
procs,
onEarlyExit,
config,
installDir: options.installDir,
extraKbnOpts: getExtraKbnOpts(options.installDir, config.get('serverless')),
});
// wait for 5 seconds
await silence(log, 5000);
// Running 'npx playwright test --config=${playwrightConfigPath}'
await procs.run(`playwright`, {
cmd: resolve(REPO_ROOT, './node_modules/.bin/playwright'),
args: ['test', `--config=${playwrightConfigPath}`, ...(options.headed ? ['--headed'] : [])],
cwd: resolve(REPO_ROOT),
env: {
...process.env,
},
wait: true,
});
} finally {
try {
await procs.stop('kibana');
} finally {
if (shutdownEs) {
await shutdownEs();
}
}
}
reportTime(runStartTime, 'ready', {
success: true,
...options,
});
});
}

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", 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 { PlaywrightTestConfig, PlaywrightTestOptions } from 'playwright/test';
export type Protocol = 'http' | 'https';
export const VALID_CONFIG_MARKER = Symbol('validConfig');
export interface ScoutTestOptions extends PlaywrightTestOptions {
serversConfigDir: string;
[VALID_CONFIG_MARKER]: boolean;
}
export interface ScoutPlaywrightOptions extends Pick<PlaywrightTestConfig, 'testDir' | 'workers'> {
testDir: string;
workers?: 1 | 2;
}

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", 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 const serviceLoadedMsg = (name: string) => `scout service loaded: ${name}`;

View file

@ -0,0 +1,55 @@
/*
* 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 { v4 as uuidV4 } from 'uuid';
import { resolve } from 'path';
import { FlagsReader, FlagOptions } from '@kbn/dev-cli-runner';
import { createFlagError } from '@kbn/dev-cli-errors';
import { REPO_ROOT } from '@kbn/repo-info';
import { CliSupportedServerModes } from '../types';
export type StartServerOptions = ReturnType<typeof parseServerFlags>;
export const SERVER_FLAG_OPTIONS: FlagOptions = {
string: ['serverless', 'esFrom', 'kibana-install-dir'],
boolean: ['stateful', 'logToFile'],
help: `
--stateful Start Elasticsearch and Kibana with default ESS configuration
--serverless Start Elasticsearch and Kibana with serverless project configuration: es | oblt | security
--esFrom Build Elasticsearch from source or run snapshot or serverless. Default: $TEST_ES_FROM or "snapshot"
--kibana-install-dir Run Kibana from existing install directory instead of from source
--logToFile Write the log output from Kibana/ES to files instead of to stdout
`,
};
export function parseServerFlags(flags: FlagsReader) {
const serverlessType = flags.enum('serverless', ['es', 'oblt', 'security']);
const isStateful = flags.boolean('stateful');
if (!(serverlessType || isStateful) || (serverlessType && isStateful)) {
throw createFlagError(`Expected exactly one of --serverless=<type> or --stateful flag`);
}
const mode: CliSupportedServerModes = serverlessType
? `serverless=${serverlessType}`
: 'stateful';
const esFrom = flags.enum('esFrom', ['source', 'snapshot', 'serverless']);
const installDir = flags.string('kibana-install-dir');
const logsDir = flags.boolean('logToFile')
? resolve(REPO_ROOT, 'data/ftr_servers_logs', uuidV4())
: undefined;
return {
mode,
esFrom,
installDir,
logsDir,
};
}

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 { parseServerFlags, SERVER_FLAG_OPTIONS } from './flags';
export { startServers } from './start_servers';
export { runKibanaServer } from './run_kibana_server';
export { runElasticsearch } from './run_elasticsearch';
export type { StartServerOptions } from './flags';

View file

@ -0,0 +1,194 @@
/*
* 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 Url from 'url';
import { resolve } from 'path';
import type { ToolingLog } from '@kbn/tooling-log';
import { REPO_ROOT } from '@kbn/repo-info';
import type { ArtifactLicense, ServerlessProjectType } from '@kbn/es';
import { isServerlessProjectType, extractAndArchiveLogs } from '@kbn/es/src/utils';
import { createTestEsCluster, esTestConfig } from '@kbn/test';
import { Config } from '../config';
interface RunElasticsearchOptions {
log: ToolingLog;
esFrom?: string;
esServerlessImage?: string;
config: Config;
onEarlyExit?: (msg: string) => void;
logsDir?: string;
name?: string;
}
type EsConfig = ReturnType<typeof getEsConfig>;
function getEsConfig({
config,
esFrom = config.get('esTestCluster.from'),
esServerlessImage,
}: RunElasticsearchOptions) {
const ssl = !!config.get('esTestCluster.ssl');
const license: ArtifactLicense = config.get('esTestCluster.license');
const esArgs: string[] = config.get('esTestCluster.serverArgs');
const esJavaOpts: string | undefined = config.get('esTestCluster.esJavaOpts');
const isSecurityEnabled = esArgs.includes('xpack.security.enabled=true');
const port: number | undefined = config.get('servers.elasticsearch.port');
const password: string | undefined = isSecurityEnabled
? 'changeme'
: config.get('servers.elasticsearch.password');
const dataArchive: string | undefined = config.get('esTestCluster.dataArchive');
const serverless: boolean = config.get('serverless');
const files: string[] | undefined = config.get('esTestCluster.files');
const esServerlessOptions = serverless
? getESServerlessOptions(esServerlessImage, config)
: undefined;
return {
ssl,
license,
esArgs,
esJavaOpts,
isSecurityEnabled,
esFrom,
esServerlessOptions,
port,
password,
dataArchive,
serverless,
files,
};
}
export async function runElasticsearch(
options: RunElasticsearchOptions
): Promise<() => Promise<void>> {
const { log, logsDir, name } = options;
const config = getEsConfig(options);
const node = await startEsNode({
log,
name: name ?? 'scout',
logsDir,
config,
});
return async () => {
await node.cleanup();
await extractAndArchiveLogs({ outputFolder: logsDir, log });
};
}
async function startEsNode({
log,
name,
config,
onEarlyExit,
logsDir,
}: {
log: ToolingLog;
name: string;
config: EsConfig & { transportPort?: number };
onEarlyExit?: (msg: string) => void;
logsDir?: string;
}) {
const cluster = createTestEsCluster({
clusterName: `cluster-${name}`,
esArgs: config.esArgs,
esFrom: config.esFrom,
esServerlessOptions: config.esServerlessOptions,
esJavaOpts: config.esJavaOpts,
license: config.license,
password: config.password,
port: config.port,
ssl: config.ssl,
log,
writeLogsToPath: logsDir ? resolve(logsDir, `es-cluster-${name}.log`) : undefined,
basePath: resolve(REPO_ROOT, '.es'),
nodes: [
{
name,
dataArchive: config.dataArchive,
},
],
transportPort: config.transportPort,
onEarlyExit,
serverless: config.serverless,
files: config.files,
});
await cluster.start();
return cluster;
}
interface EsServerlessOptions {
projectType: ServerlessProjectType;
host?: string;
resources: string[];
kibanaUrl: string;
tag?: string;
image?: string;
}
function getESServerlessOptions(
esServerlessImageFromArg: string | undefined,
config: Config
): EsServerlessOptions {
const esServerlessImageUrlOrTag =
esServerlessImageFromArg ||
esTestConfig.getESServerlessImage() ||
(config.has('esTestCluster.esServerlessImage') &&
config.get('esTestCluster.esServerlessImage'));
const serverlessResources: string[] =
(config.has('esServerlessOptions.resources') && config.get('esServerlessOptions.resources')) ||
[];
const serverlessHost: string | undefined =
config.has('esServerlessOptions.host') && config.get('esServerlessOptions.host');
const kbnServerArgs =
(config.has('kbnTestServer.serverArgs') &&
(config.get('kbnTestServer.serverArgs') as string[])) ||
[];
const projectType = kbnServerArgs
.filter((arg) => arg.startsWith('--serverless'))
.reduce((acc, arg) => {
const match = arg.match(/--serverless[=\s](\w+)/);
return acc + (match ? match[1] : '');
}, '') as ServerlessProjectType;
if (!isServerlessProjectType(projectType)) {
throw new Error(`Unsupported serverless projectType: ${projectType}`);
}
const commonOptions = {
projectType,
host: serverlessHost,
resources: serverlessResources,
kibanaUrl: Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
}),
};
if (esServerlessImageUrlOrTag) {
return {
...commonOptions,
...(esServerlessImageUrlOrTag.includes(':')
? { image: esServerlessImageUrlOrTag }
: { tag: esServerlessImageUrlOrTag }),
};
}
return commonOptions;
}

View file

@ -0,0 +1,135 @@
/*
* 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 Path from 'path';
import Os from 'os';
import { v4 as uuidv4 } from 'uuid';
import type { ProcRunner } from '@kbn/dev-proc-runner';
import { REPO_ROOT } from '@kbn/repo-info';
import { parseRawFlags, getArgValue, remapPluginPaths, DedicatedTaskRunner } from '@kbn/test';
import { Config } from '../config';
export async function runKibanaServer(options: {
procs: ProcRunner;
config: Config;
installDir?: string;
extraKbnOpts?: string[];
logsDir?: string;
onEarlyExit?: (msg: string) => void;
}) {
const { config, procs } = options;
const runOptions = options.config.get('kbnTestServer.runOptions');
const installDir = runOptions.alwaysUseSource ? undefined : options.installDir;
const devMode = !installDir;
const useTaskRunner = options.config.get('kbnTestServer.useDedicatedTaskRunner');
const procRunnerOpts = {
cwd: installDir || REPO_ROOT,
cmd: installDir
? process.platform.startsWith('win')
? Path.resolve(installDir, 'bin/kibana.bat')
: Path.resolve(installDir, 'bin/kibana')
: process.execPath,
env: {
FORCE_COLOR: 1,
...process.env,
...options.config.get('kbnTestServer.env'),
},
wait: runOptions.wait,
onEarlyExit: options.onEarlyExit,
};
const prefixArgs = devMode
? [Path.relative(procRunnerOpts.cwd, Path.resolve(REPO_ROOT, 'scripts/kibana'))]
: [];
const buildArgs: string[] = config.get('kbnTestServer.buildArgs') || [];
const sourceArgs: string[] = config.get('kbnTestServer.sourceArgs') || [];
const serverArgs: string[] = config.get('kbnTestServer.serverArgs') || [];
let kbnFlags = parseRawFlags([
// When installDir is passed, we run from a built version of Kibana which uses different command line
// arguments. If installDir is not passed, we run from source code.
...(installDir ? [...buildArgs, ...serverArgs] : [...sourceArgs, ...serverArgs]),
// We also allow passing in extra Kibana server options, tack those on here so they always take precedence
...(options.extraKbnOpts ?? []),
]);
if (installDir) {
kbnFlags = remapPluginPaths(kbnFlags, installDir);
}
const mainName = useTaskRunner ? 'kbn-ui' : 'kibana';
const promises = [
// main process
procs.run(mainName, {
...procRunnerOpts,
writeLogsToPath: options.logsDir
? Path.resolve(options.logsDir, `${mainName}.log`)
: undefined,
args: [
...prefixArgs,
...parseRawFlags([
...kbnFlags,
...(!useTaskRunner
? []
: [
'--node.roles=["ui"]',
`--path.data=${Path.resolve(Os.tmpdir(), `scout-ui-${uuidv4()}`)}`,
]),
]),
],
}),
];
if (useTaskRunner) {
const mainUuid = getArgValue(kbnFlags, 'server.uuid');
// dedicated task runner
promises.push(
procs.run('kbn-tasks', {
...procRunnerOpts,
writeLogsToPath: options.logsDir
? Path.resolve(options.logsDir, 'kbn-tasks.log')
: undefined,
args: [
...prefixArgs,
...parseRawFlags([
...kbnFlags,
`--server.port=${DedicatedTaskRunner.getPort(config.get('servers.kibana.port'))}`,
'--node.roles=["background_tasks"]',
`--path.data=${Path.resolve(Os.tmpdir(), `ftr-task-runner-${uuidv4()}`)}`,
...(typeof mainUuid === 'string' && mainUuid
? [`--server.uuid=${DedicatedTaskRunner.getUuid(mainUuid)}`]
: []),
...(devMode ? ['--no-optimizer'] : []),
]),
],
})
);
}
await Promise.all(promises);
}
export function getExtraKbnOpts(installDir: string | undefined, isServerless: boolean) {
if (installDir) {
return [];
}
return [
'--dev',
'--no-dev-config',
'--no-dev-credentials',
isServerless
? '--server.versioned.versionResolution=newest'
: '--server.versioned.versionResolution=oldest',
];
}

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", 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 dedent from 'dedent';
import { ToolingLog } from '@kbn/tooling-log';
import { withProcRunner } from '@kbn/dev-proc-runner';
import { getTimeReporter } from '@kbn/ci-stats-reporter';
import { runElasticsearch } from './run_elasticsearch';
import { getExtraKbnOpts, runKibanaServer } from './run_kibana_server';
import { StartServerOptions } from './flags';
import { loadServersConfig } from '../config';
import { silence } from '../common';
export async function startServers(log: ToolingLog, options: StartServerOptions) {
const runStartTime = Date.now();
const reportTime = getTimeReporter(log, 'scripts/scout_start_servers');
await withProcRunner(log, async (procs) => {
const config = await loadServersConfig(options.mode, log);
const shutdownEs = await runElasticsearch({
config,
log,
esFrom: options.esFrom,
logsDir: options.logsDir,
});
await runKibanaServer({
procs,
config,
installDir: options.installDir,
extraKbnOpts: getExtraKbnOpts(options.installDir, config.get('serverless')),
});
reportTime(runStartTime, 'ready', {
success: true,
...options,
});
// wait for 5 seconds of silence before logging the
// success message so that it doesn't get buried
await silence(log, 5000);
log.success(
'\n\n' +
dedent`
Elasticsearch and Kibana are ready for functional testing.
Use 'npx playwright test --config <path_to_Playwright.config.ts>' to run tests'
` +
'\n\n'
);
await procs.waitForAllToStop();
await shutdownEs();
});
}

14
packages/kbn-scout/src/types/cli.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
/*
* 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 CliSupportedServerModes =
| 'stateful'
| 'serverless=es'
| 'serverless=oblt'
| 'serverless=security';

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 { UrlParts } from '@kbn/test';
export interface ScoutLoaderConfig {
serverless?: boolean;
servers: {
kibana: UrlParts;
elasticsearch: UrlParts;
fleet?: UrlParts;
};
dockerServers: any;
esTestCluster: {
from: string;
license?: string;
files: string[];
serverArgs: string[];
ssl: boolean;
};
kbnTestServer: {
env?: any;
buildArgs?: string[];
sourceArgs?: string[];
serverArgs: string[];
useDedicatedTastRunner?: boolean;
};
}

View file

@ -0,0 +1,12 @@
/*
* 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 * from './config';
export * from './cli';
export * from './servers';

View file

@ -0,0 +1,26 @@
/*
* 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 { ServerlessProjectType } from '@kbn/es';
export interface ScoutServerConfig {
serverless: boolean;
projectType?: ServerlessProjectType;
isCloud: boolean;
cloudUsersFilePath: string;
hosts: {
kibana: string;
elasticsearch: string;
};
auth: {
username: string;
password: string;
};
metadata?: any;
}

View file

@ -0,0 +1,31 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/tooling-log",
"@kbn/dev-cli-runner",
"@kbn/dev-cli-errors",
"@kbn/ci-stats-reporter",
"@kbn/repo-info",
"@kbn/es",
"@kbn/dev-proc-runner",
"@kbn/test",
"@kbn/es-archiver",
"@kbn/dev-utils",
"@kbn/mock-idp-utils",
"@kbn/test-suites-xpack",
"@kbn/test-subj-selector",
]
}

View file

@ -14,14 +14,23 @@ export { startServersCli, startServers } from './src/functional_tests/start_serv
// @internal
export { runTestsCli, runTests } from './src/functional_tests/run_tests';
export {
runElasticsearch,
runKibanaServer,
parseRawFlags,
getArgValue,
remapPluginPaths,
getKibanaCliArg,
getKibanaCliLoggers,
} from './src/functional_tests/lib';
export { initLogsDir } from './src/functional_tests/lib';
export {
SamlSessionManager,
type SamlSessionManagerOptions,
type HostOptions,
type GetCookieOptions,
} from './src/auth';
export { runElasticsearch, runKibanaServer } from './src/functional_tests/lib';
export { getKibanaCliArg, getKibanaCliLoggers } from './src/functional_tests/lib/kibana_cli_args';
export type {
CreateTestEsClusterOptions,
@ -38,6 +47,7 @@ export {
} from './src/es';
export { kbnTestConfig } from './kbn_test_config';
export type { UrlParts } from './kbn_test_config';
export {
kibanaServerTestUser,

View file

@ -16,6 +16,7 @@ export {
Lifecycle,
LifecyclePhase,
runCheckFtrConfigsCli,
DedicatedTaskRunner,
} from './lib';
export { runFtrCli } from './cli';
export * from './lib/docker_servers';

View file

@ -50,6 +50,11 @@ export async function runCheckFtrConfigsCli() {
return false;
}
// playwright config files
if (file.match(/\/ui_tests\/*playwright*.config.ts$/)) {
return false;
}
if (!file.match(/(test|e2e).*config[^\/]*\.(t|j)s$/)) {
return false;
}

View file

@ -10,3 +10,11 @@
export { runKibanaServer } from './run_kibana_server';
export { runElasticsearch } from './run_elasticsearch';
export * from './run_ftr';
export {
parseRawFlags,
getArgValue,
remapPluginPaths,
getKibanaCliArg,
getKibanaCliLoggers,
} from './kibana_cli_args';
export { initLogsDir } from './logs_dir';

View file

@ -0,0 +1,11 @@
/*
* 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".
*/
require('../src/setup_node_env');
require('@kbn/scout').startServersCli();

11
scripts/scout_test.js Normal file
View file

@ -0,0 +1,11 @@
/*
* 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".
*/
require('../src/setup_node_env');
require('@kbn/scout').runTestsCli();

View file

@ -1534,6 +1534,8 @@
"@kbn/saved-objects-tagging-plugin/*": ["x-pack/plugins/saved_objects_tagging/*"],
"@kbn/saved-search-plugin": ["src/plugins/saved_search"],
"@kbn/saved-search-plugin/*": ["src/plugins/saved_search/*"],
"@kbn/scout": ["packages/kbn-scout"],
"@kbn/scout/*": ["packages/kbn-scout/*"],
"@kbn/screenshot-mode-example-plugin": ["examples/screenshot_mode_example"],
"@kbn/screenshot-mode-example-plugin/*": ["examples/screenshot_mode_example/*"],
"@kbn/screenshot-mode-plugin": ["src/plugins/screenshot_mode"],

1
x-pack/.gitignore vendored
View file

@ -12,3 +12,4 @@
/.env
/.kibana-plugin-helpers.dev.*
.cache
**/ui_tests/output

View file

@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "target/types",
},
"include": ["*.ts", "common/**/*", "public/**/*", "server/**/*"],
"include": ["*.ts", "common/**/*", "public/**/*", "server/**/*", "ui_tests/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/data-plugin",
@ -21,6 +21,7 @@
"@kbn/presentation-publishing",
"@kbn/data-views-plugin",
"@kbn/unified-search-plugin",
"@kbn/scout",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,17 @@
## How to run tests
First start the servers with
```bash
// ESS
node scripts/scout_start_servers.js --stateful
// Serverless
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
```
Test results are available in `x-pack/plugins/discover_enhanced/ui_tests/output`

View file

@ -0,0 +1,32 @@
/*
* 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 {
test as base,
PageObjects,
createLazyPageObject,
ScoutTestFixtures,
ScoutWorkerFixtures,
} from '@kbn/scout';
import { DemoPage } from './page_objects';
interface ExtendedScoutTestFixtures extends ScoutTestFixtures {
pageObjects: PageObjects & {
demo: DemoPage;
};
}
export const test = base.extend<ExtendedScoutTestFixtures, ScoutWorkerFixtures>({
pageObjects: async ({ pageObjects, page }, use) => {
const extendedPageObjects = {
...pageObjects,
demo: createLazyPageObject(DemoPage, page),
};
await use(extendedPageObjects);
},
});

View file

@ -0,0 +1,16 @@
/*
* 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 { ScoutPage } from '@kbn/scout';
export class DemoPage {
constructor(private readonly page: ScoutPage) {}
async goto() {
this.page.gotoApp('not_implemented');
}
}

View file

@ -0,0 +1,8 @@
/*
* 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 { DemoPage } from './demo';

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; 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({
testDir: './tests',
});

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { expect } from '@kbn/scout';
import { test } 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',
});
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',
});
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');
});
});

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.
*/
import { expect } from '@kbn/scout';
import { test } 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.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('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"');
});
});

View file

@ -6876,6 +6876,10 @@
version "0.0.0"
uid ""
"@kbn/scout@link:packages/kbn-scout":
version "0.0.0"
uid ""
"@kbn/screenshot-mode-example-plugin@link:examples/screenshot_mode_example":
version "0.0.0"
uid ""