[scout] migrate more Discover tests (#201842)

## Summary

This PR migrates the following FTR tests to `@kbn/scout`:

`x-pack/test/functional/apps/discover/error_handling.ts` =>
`x-pack/plugins/discover_enhanced/ui_tests/tests/error_handling.spec.ts`

`x-pack/test/functional/apps/discover/saved_search_embeddable.ts` =>

`x-pack/plugins/discover_enhanced/ui_tests/tests/saved_search_embeddable.spec.ts`

`x-pack/test/functional/apps/discover/saved_searches.ts` =>
`x-pack/plugins/discover_enhanced/ui_tests/tests/saved_searches.spec.ts`

`x-pack/test/functional/apps/discover/value_suggestions.ts` 2nd describe
block =>

`x-pack/plugins/discover_enhanced/ui_tests/tests/value_suggestions_use_time_range_disabled.spec.ts`

Some other changes to mention:

**packages/kbn-test-subj-selector**:
- support of `^foo` syntax similar to `CSS [attribute^=value] Selector`

**packages/kbn-scout**:
- new worker fixture `uiSettings` to wrap Advanced Settings set/unset
capability
- extend `ScoutPage` fixture with `typeWithDelay` method required for
many Kibana input fields
- extend `PageObjects` fixture with `DashboardApp` & `FilterBar`, also
extending existing ones.

How to test:

```bash
// ESS
node scripts/scout_start_servers.js --stateful
npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts --grep @ess

// Serverless
node scripts/scout_start_servers.js --serverless=es
npx playwright test --config x-pack/plugins/discover_enhanced/ui_tests/playwright.config.ts --grep @svlSearch
```

---------

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ import { ScoutPage, KibanaUrl } from '../types';
* Instead of defining each method individually, we use a list of method names and loop through them, creating methods dynamically.
* All methods must have 'selector: string' as the first argument
*/
function extendPageWithTestSubject(page: Page) {
function extendPageWithTestSubject(page: Page): ScoutPage['testSubj'] {
const methods: Array<keyof Page> = [
'check',
'click',
@ -28,10 +28,15 @@ function extendPageWithTestSubject(page: Page) {
'innerText',
'isChecked',
'isHidden',
'isVisible',
'locator',
'waitForSelector',
];
const extendedMethods: Partial<Record<keyof Page, Function>> = {};
const extendedMethods: Partial<Record<keyof Page, Function>> & {
typeWithDelay?: ScoutPage['testSubj']['typeWithDelay'];
clearInput?: ScoutPage['testSubj']['clearInput'];
} = {};
for (const method of methods) {
extendedMethods[method] = (...args: any[]) => {
@ -41,7 +46,27 @@ function extendPageWithTestSubject(page: Page) {
};
}
return extendedMethods as Record<keyof Page, any>;
// custom method to types text into an input field character by character with a delay
extendedMethods.typeWithDelay = async (
selector: string,
text: string,
options?: { delay: number }
) => {
const { delay = 25 } = options || {};
const testSubjSelector = subj(selector);
await page.locator(testSubjSelector).click();
for (const char of text) {
await page.keyboard.insertText(char);
await page.waitForTimeout(delay);
}
};
// custom method to clear an input field
extendedMethods.clearInput = async (selector: string) => {
const testSubjSelector = subj(selector);
await page.locator(testSubjSelector).fill('');
};
return extendedMethods as ScoutPage['testSubj'];
}
/**
@ -78,6 +103,9 @@ export const scoutPageFixture = base.extend<{ page: ScoutPage; kbnUrl: KibanaUrl
// Method to navigate to specific Kibana apps
page.gotoApp = (appName: string) => page.goto(kbnUrl.app(appName));
page.waitForLoadingIndicatorHidden = () =>
page.testSubj.waitForSelector('globalLoadingIndicator-hidden', { state: 'attached' });
await use(page);
},
});

View file

@ -22,8 +22,30 @@ export interface LoginFixture {
loginAsPrivilegedUser: () => Promise<void>;
}
/**
* Extends the Playwright 'Page' interface with methods specific to Kibana.
* Reasons to use 'ReturnType' instead of Explicit Typings:
* - DRY Principle: automatically stays in sync with the Playwright API, reducing maintenance overhead.
* - Future-Proofing: If Playwright changes the return type of methods, these types will update accordingly.
* Recommendation: define Explicit Types as return types only if methods (e.g. 'typeWithDelay')
* have any additional logic or Kibana-specific behavior.
*/
export type ScoutPage = Page & {
/**
* Navigates to the specified Kibana application.
* @param appName - The name of the Kibana app (e.g., 'discover', 'dashboard').
* @param options - Additional navigation options, passed directly to Playwright's `goto` method.
* @returns A Promise resolving to a Playwright `Response` or `null`.
*/
gotoApp: (appName: string, options?: Parameters<Page['goto']>[1]) => ReturnType<Page['goto']>;
/**
* Waits for the Kibana loading spinner indicator to disappear.
* @returns A Promise resolving when the indicator is hidden.
*/
waitForLoadingIndicatorHidden: () => ReturnType<Page['waitForSelector']>;
/**
* Simplified API to interact with elements using Kibana's 'data-test-subj' attribute.
*/
testSubj: {
check: (selector: string, options?: Parameters<Page['check']>[1]) => ReturnType<Page['check']>;
click: (selector: string, options?: Parameters<Page['click']>[1]) => ReturnType<Page['click']>;
@ -59,9 +81,34 @@ export type ScoutPage = Page & {
selector: string,
options?: Parameters<Page['isHidden']>[1]
) => ReturnType<Page['isHidden']>;
isVisible: (
selector: string,
options?: Parameters<Page['isVisible']>[1]
) => ReturnType<Page['isVisible']>;
locator: (
selector: string,
options?: Parameters<Page['locator']>[1]
) => ReturnType<Page['locator']>;
waitForSelector: (
selector: string,
options?: Parameters<Page['waitForSelector']>[1]
) => ReturnType<Page['waitForSelector']>;
// custom methods
/**
* Types text into an input field character by character with a specified delay between each character.
*
* @param selector - The selector for the input element (supports 'data-test-subj' attributes).
* @param text - The text to type into the input field.
* @param options - Optional configuration object.
* @param options.delay - The delay in milliseconds between typing each character (default: 25ms).
* @returns A Promise that resolves once the text has been typed.
*/
typeWithDelay: (selector: string, text: string, options?: { delay: number }) => Promise<void>;
/**
* Clears the input field by filling it with an empty string.
* @param selector The selector for the input element (supports 'data-test-subj' attributes).
* @returns A Promise that resolves once the text has been cleared.
*/
clearInput: (selector: string) => Promise<void>;
};
};

View file

@ -12,23 +12,31 @@ import type { ToolingLog } from '@kbn/tooling-log';
import type { Client } from '@elastic/elasticsearch';
import { LoadActionPerfOptions } from '@kbn/es-archiver';
import { IndexStats } from '@kbn/es-archiver/src/lib/stats';
import type { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings';
import { ScoutServerConfig } from '../../../types';
import { KibanaUrl } from '../../../common/services/kibana_url';
interface EsArchiverFixture {
export interface EsArchiverFixture {
loadIfNeeded: (
name: string,
performance?: LoadActionPerfOptions | undefined
) => Promise<Record<string, IndexStats>>;
}
export interface UiSettingsFixture {
set: (values: UiSettingValues) => Promise<void>;
unset: (...values: string[]) => Promise<any>;
setDefaultTime: ({ from, to }: { from: string; to: string }) => Promise<void>;
}
export interface ScoutWorkerFixtures {
log: ToolingLog;
config: ScoutServerConfig;
kbnUrl: KibanaUrl;
esClient: Client;
kbnClient: KbnClient;
uiSettings: UiSettingsFixture;
esArchiver: EsArchiverFixture;
samlAuth: SamlSessionManager;
}

View file

@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { test as base } from '@playwright/test';
import { LoadActionPerfOptions } from '@kbn/es-archiver';
import {
createKbnUrl,
createEsArchiver,
createEsClient,
createKbnClient,
createLogger,
createSamlSessionManager,
createScoutConfig,
} from '../../../common/services';
import { ScoutWorkerFixtures } from '../types/worker_scope';
import { ScoutTestOptions } from '../../types';
export const coreWorkerFixtures = base.extend<{}, ScoutWorkerFixtures>({
log: [
({}, use) => {
use(createLogger());
},
{ scope: 'worker' },
],
config: [
({ log }, use, testInfo) => {
const configName = 'local';
const projectUse = testInfo.project.use as ScoutTestOptions;
const serversConfigDir = projectUse.serversConfigDir;
const configInstance = createScoutConfig(serversConfigDir, configName, log);
use(configInstance);
},
{ scope: 'worker' },
],
kbnUrl: [
({ config, log }, use) => {
use(createKbnUrl(config, log));
},
{ scope: 'worker' },
],
esClient: [
({ config, log }, use) => {
use(createEsClient(config, log));
},
{ scope: 'worker' },
],
kbnClient: [
({ log, config }, use) => {
use(createKbnClient(config, log));
},
{ scope: 'worker' },
],
esArchiver: [
({ log, esClient, kbnClient }, use) => {
const esArchiverInstance = createEsArchiver(esClient, kbnClient, log);
// to speedup test execution we only allow to ingest the data indexes and only if index doesn't exist
const loadIfNeeded = async (name: string, performance?: LoadActionPerfOptions | undefined) =>
esArchiverInstance!.loadIfNeeded(name, performance);
use({ loadIfNeeded });
},
{ scope: 'worker' },
],
samlAuth: [
({ log, config }, use) => {
use(createSamlSessionManager(config, log));
},
{ scope: 'worker' },
],
});

View file

@ -7,78 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { test as base } from '@playwright/test';
import { mergeTests } from 'playwright/test';
import { uiSettingsFixture } from './ui_settings';
import { coreWorkerFixtures } from './core';
import { LoadActionPerfOptions } from '@kbn/es-archiver';
import {
createKbnUrl,
createEsArchiver,
createEsClient,
createKbnClient,
createLogger,
createSamlSessionManager,
createScoutConfig,
} from '../../../common/services';
import { ScoutWorkerFixtures } from '../types/worker_scope';
import { ScoutTestOptions } from '../../types';
export const scoutWorkerFixtures = base.extend<{}, ScoutWorkerFixtures>({
log: [
({}, use) => {
use(createLogger());
},
{ scope: 'worker' },
],
config: [
({ log }, use, testInfo) => {
const configName = 'local';
const projectUse = testInfo.project.use as ScoutTestOptions;
const serversConfigDir = projectUse.serversConfigDir;
const configInstance = createScoutConfig(serversConfigDir, configName, log);
use(configInstance);
},
{ scope: 'worker' },
],
kbnUrl: [
({ config, log }, use) => {
use(createKbnUrl(config, log));
},
{ scope: 'worker' },
],
esClient: [
({ config, log }, use) => {
use(createEsClient(config, log));
},
{ scope: 'worker' },
],
kbnClient: [
({ log, config }, use) => {
use(createKbnClient(config, log));
},
{ scope: 'worker' },
],
esArchiver: [
({ log, esClient, kbnClient }, use) => {
const esArchiverInstance = createEsArchiver(esClient, kbnClient, log);
// to speedup test execution we only allow to ingest the data indexes and only if index doesn't exist
const loadIfNeeded = async (name: string, performance?: LoadActionPerfOptions | undefined) =>
esArchiverInstance!.loadIfNeeded(name, performance);
use({ loadIfNeeded });
},
{ scope: 'worker' },
],
samlAuth: [
({ log, config }, use) => {
use(createSamlSessionManager(config, log));
},
{ scope: 'worker' },
],
});
export const scoutWorkerFixtures = mergeTests(coreWorkerFixtures, uiSettingsFixture);

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { test as base } from '@playwright/test';
import { UiSettingValues } from '@kbn/test/src/kbn_client/kbn_client_ui_settings';
import { ScoutWorkerFixtures } from '../types';
import { isValidUTCDate, formatTime } from '../../utils';
export const uiSettingsFixture = base.extend<{}, ScoutWorkerFixtures>({
uiSettings: [
({ kbnClient }, use) => {
const kbnClientUiSettings = {
set: async (values: UiSettingValues) => kbnClient.uiSettings.update(values),
unset: async (...keys: string[]) =>
Promise.all(keys.map((key) => kbnClient.uiSettings.unset(key))),
setDefaultTime: async ({ from, to }: { from: string; to: string }) => {
const utcFrom = isValidUTCDate(from) ? from : formatTime(from);
const untcTo = isValidUTCDate(to) ? to : formatTime(to);
await kbnClient.uiSettings.update({
'timepicker:timeDefaults': `{ "from": "${utcFrom}", "to": "${untcTo}"}`,
});
},
};
use(kbnClientUiSettings);
},
{ scope: 'worker' },
],
});

View file

@ -19,4 +19,9 @@ export { expect } from './expect';
export type { ScoutPlaywrightOptions, ScoutTestOptions } from './types';
export type { PageObjects } from './page_objects';
export type { ScoutTestFixtures, ScoutWorkerFixtures, ScoutPage } from './fixtures';
export type {
ScoutTestFixtures,
ScoutWorkerFixtures,
ScoutPage,
EsArchiverFixture,
} from './fixtures';

View file

@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../fixtures/types';
type CommonlyUsedTimeRange =
| 'Today'
| 'Last_15 minutes'
| 'Last_1 hour'
| 'Last_24 hours'
| 'Last_30 days'
| 'Last_90 days'
| 'Last_1 year';
export class DashboardApp {
constructor(private readonly page: ScoutPage) {}
async goto() {
await this.page.gotoApp('dashboards');
}
async openNewDashboard() {
await this.page.testSubj.click('newItemButton');
await this.page.testSubj.waitForSelector('emptyDashboardWidget', { state: 'visible' });
}
async saveDashboard(name: string) {
await this.page.testSubj.click('dashboardInteractiveSaveMenuItem');
await this.page.testSubj.fill('savedObjectTitle', name);
await this.page.testSubj.click('confirmSaveSavedObjectButton');
await this.page.testSubj.waitForSelector('confirmSaveSavedObjectButton', { state: 'hidden' });
}
async addPanelFromLibrary(...names: string[]) {
await this.page.testSubj.click('dashboardAddFromLibraryButton');
for (let i = 0; i < names.length; i++) {
// clear search input after the first panel is added
if (i > 0) {
await this.page.testSubj.clearInput('savedObjectFinderSearchInput');
}
await this.page.testSubj.typeWithDelay('savedObjectFinderSearchInput', names[i]);
await this.page.testSubj.click(`savedObjectTitle${names[i].replace(/ /g, '-')}`);
// wait for the panel to be added
await this.page.testSubj.waitForSelector(
`embeddablePanelHeading-${names[i].replace(/[- ]/g, '')}`,
{
state: 'visible',
}
);
}
// close the flyout
await this.page.testSubj.click('euiFlyoutCloseButton');
await this.page.testSubj.waitForSelector('euiFlyoutCloseButton', { state: 'hidden' });
}
async customizePanel(options: {
name: string;
customTimeRageCommonlyUsed?: {
value: CommonlyUsedTimeRange;
};
}) {
await this.page.testSubj.hover(`embeddablePanelHeading-${options.name.replace(/ /g, '')}`);
await this.page.testSubj.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL');
if (options.customTimeRageCommonlyUsed) {
await this.page.testSubj.click('customizePanelShowCustomTimeRange');
await this.page.testSubj.click(
'customizePanelTimeRangeDatePicker > superDatePickerToggleQuickMenuButton'
);
await this.page.testSubj.click(
`superDatePickerCommonlyUsed_${options.customTimeRageCommonlyUsed.value}`
);
}
await this.page.testSubj.click('saveCustomizePanelButton');
await this.page.testSubj.waitForSelector('saveCustomizePanelButton', { state: 'hidden' });
}
async removePanel(name: string | 'embeddableError') {
const panelHeaderTestSubj =
name === 'embeddableError' ? name : `embeddablePanelHeading-${name.replace(/ /g, '')}`;
await this.page.testSubj.locator(panelHeaderTestSubj).scrollIntoViewIfNeeded();
await this.page.testSubj.locator(panelHeaderTestSubj).hover();
await this.page.testSubj.click('embeddablePanelToggleMenuIcon');
await this.page.testSubj.click('embeddablePanelAction-deletePanel');
await this.page.testSubj.waitForSelector(panelHeaderTestSubj, {
state: 'hidden',
});
}
}

View file

@ -13,8 +13,36 @@ import { expect } from '..';
export class DatePicker {
constructor(private readonly page: ScoutPage) {}
private async showStartEndTimes() {
// This first await makes sure the superDatePicker has loaded before we check for the ShowDatesButton
await this.page.testSubj.waitForSelector('superDatePickerToggleQuickMenuButton', {
timeout: 10000,
});
const isShowDateBtnVisible = await this.page.testSubj.isVisible(
'superDatePickerShowDatesButton',
{
timeout: 5000,
}
);
if (isShowDateBtnVisible) {
await this.page.testSubj.click('superDatePickerShowDatesButton');
}
const isStartDatePopoverBtnVisible = await this.page.testSubj.isVisible(
'superDatePickerstartDatePopoverButton',
{
timeout: 5000,
}
);
if (isStartDatePopoverBtnVisible) {
await this.page.testSubj.click('superDatePickerstartDatePopoverButton');
}
}
async setAbsoluteRange({ from, to }: { from: string; to: string }) {
await this.page.testSubj.click('superDatePickerShowDatesButton');
await this.showStartEndTimes();
// we start with end date
await this.page.testSubj.click('superDatePickerendDatePopoverButton');
await this.page.testSubj.click('superDatePickerAbsoluteTab');
@ -32,10 +60,14 @@ export class DatePicker {
await this.page.testSubj.click('parseAbsoluteDateFormat');
await this.page.keyboard.press('Escape');
await expect(this.page.testSubj.locator('superDatePickerstartDatePopoverButton')).toHaveText(
from
);
await expect(this.page.testSubj.locator('superDatePickerendDatePopoverButton')).toHaveText(to);
await expect(
this.page.testSubj.locator('superDatePickerstartDatePopoverButton'),
`Date picker 'start date' should be set correctly`
).toHaveText(from);
await expect(
this.page.testSubj.locator('superDatePickerendDatePopoverButton'),
`Date picker 'end date' should be set correctly`
).toHaveText(to);
await this.page.testSubj.click('querySubmitButton');
}
}

View file

@ -15,4 +15,35 @@ export class DiscoverApp {
async goto() {
await this.page.gotoApp('discover');
}
async selectDataView(name: string) {
const currentValue = await this.page.testSubj.innerText('*dataView-switch-link');
if (currentValue === name) {
return;
}
await this.page.testSubj.click('*dataView-switch-link');
await this.page.testSubj.waitForSelector('indexPattern-switcher');
await this.page.testSubj.typeWithDelay('indexPattern-switcher--input', name);
await this.page.testSubj.locator('indexPattern-switcher').locator(`[title="${name}"]`).click();
await this.page.testSubj.waitForSelector('indexPattern-switcher', { state: 'hidden' });
await this.page.waitForLoadingIndicatorHidden();
}
async clickNewSearch() {
await this.page.testSubj.hover('discoverNewButton');
await this.page.testSubj.click('discoverNewButton');
await this.page.testSubj.hover('unifiedFieldListSidebar__toggle-collapse'); // cancel tooltips
}
async saveSearch(name: string) {
await this.page.testSubj.click('discoverSaveButton');
await this.page.testSubj.fill('savedObjectTitle', name);
await this.page.testSubj.click('confirmSaveSavedObjectButton');
await this.page.testSubj.waitForSelector('savedObjectSaveModal', { state: 'hidden' });
await this.page.waitForLoadingIndicatorHidden();
}
async waitForHistogramRendered() {
await this.page.testSubj.waitForSelector('unifiedHistogramRendered');
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ScoutPage } from '../fixtures/types';
import { expect } from '..';
interface FilterCreationOptions {
field: string;
operator: 'is' | 'is not' | 'is one of' | 'is not one of' | 'exists' | 'does not exist';
value: string;
}
interface FilterStateOptions {
field: string;
value: string;
enabled?: boolean;
pinned?: boolean;
negated?: boolean;
}
export class FilterBar {
constructor(private readonly page: ScoutPage) {}
async addFilter(options: FilterCreationOptions) {
await this.page.testSubj.click('addFilter');
await this.page.testSubj.waitForSelector('addFilterPopover');
// set field name
await this.page.testSubj.typeWithDelay(
'filterFieldSuggestionList > comboBoxSearchInput',
options.field
);
await this.page.click(`.euiFilterSelectItem[title="${options.field}"]`);
// set operator
await this.page.testSubj.typeWithDelay(
'filterOperatorList > comboBoxSearchInput',
options.operator
);
await this.page.click(`.euiFilterSelectItem[title="${options.operator}"]`);
// set value
await this.page.testSubj.locator('filterParams').locator('input').fill(options.value);
// save filter
await this.page.testSubj.click('saveFilter');
await expect(
this.page.testSubj.locator('^filter-badge'),
'New filter badge should be displayed'
).toBeVisible();
}
async hasFilter(options: FilterStateOptions) {
const testSubjLocator = [
'~filter',
options.enabled !== undefined && `~filter-${options.enabled ? 'enabled' : 'disabled'}`,
options.field && `~filter-key-${options.field}`,
options.value && `~filter-value-${options.value}`,
options.pinned !== undefined && `~filter-${options.pinned ? 'pinned' : 'unpinned'}`,
options.negated !== undefined && (options.negated ? '~filter-negated' : ''),
]
.filter(Boolean)
.join(' & ');
return this.page.testSubj.isVisible(testSubjLocator, { strict: true });
}
}

View file

@ -8,13 +8,17 @@
*/
import { ScoutPage } from '../fixtures/types';
import { DashboardApp } from './dashboard_app';
import { DatePicker } from './date_picker';
import { DiscoverApp } from './discover_app';
import { FilterBar } from './fiter_bar';
import { createLazyPageObject } from './utils';
export interface PageObjects {
datePicker: DatePicker;
discover: DiscoverApp;
dashboard: DashboardApp;
filterBar: FilterBar;
}
/**
@ -26,7 +30,9 @@ export interface PageObjects {
export function createCorePageObjects(page: ScoutPage): PageObjects {
return {
datePicker: createLazyPageObject(DatePicker, page),
dashboard: createLazyPageObject(DashboardApp, page),
discover: createLazyPageObject(DiscoverApp, page),
filterBar: createLazyPageObject(FilterBar, page),
// Add new page objects here
};
}

View file

@ -7,4 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import moment from 'moment';
export const serviceLoadedMsg = (name: string) => `scout service loaded: ${name}`;
export const isValidUTCDate = (date: string): boolean => {
return !isNaN(Date.parse(date)) && new Date(date).toISOString() === date;
};
export function formatTime(date: string, fmt: string = 'MMM D, YYYY @ HH:mm:ss.SSS') {
return moment.utc(date, fmt).format();
}

View file

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

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export type { KbnClient, SamlSessionManager } from '@kbn/test';
export type { Client } from '@elastic/elasticsearch';
export type { KibanaUrl } from '../common/services';
export type { ToolingLog } from '@kbn/tooling-log';