From 66a06f97dfd38a35573417f2b5f1a20d9136ffc2 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Tue, 17 Aug 2021 21:47:03 +0200 Subject: [PATCH] Update onboarding interstitial to handle default Fleet assets (#108193) --- .../__snapshots__/welcome.test.tsx.snap | 4 + .../public/application/components/home.js | 10 +- .../application/components/home.test.js | 4 +- .../public/application/components/home_app.js | 3 +- .../public/application/components/welcome.tsx | 2 +- .../routes/fetch_new_instance_status.ts | 35 +++++ src/plugins/home/server/routes/index.ts | 2 + .../services/new_instance_status.test.ts | 129 ++++++++++++++++++ .../server/services/new_instance_status.ts | 67 +++++++++ test/common/config.js | 1 - test/functional/apps/home/_welcome.ts | 30 ++++ test/functional/apps/home/index.js | 1 + test/functional/page_objects/common_page.ts | 28 +++- test/functional/page_objects/home_page.ts | 4 + test/functional/services/common/browser.ts | 9 ++ .../server/services/epm/packages/install.ts | 3 +- .../test/fleet_functional/apps/home/index.ts | 17 +++ .../fleet_functional/apps/home/welcome.ts | 48 +++++++ x-pack/test/fleet_functional/config.ts | 2 +- 19 files changed, 380 insertions(+), 19 deletions(-) create mode 100644 src/plugins/home/server/routes/fetch_new_instance_status.ts create mode 100644 src/plugins/home/server/services/new_instance_status.test.ts create mode 100644 src/plugins/home/server/services/new_instance_status.ts create mode 100644 test/functional/apps/home/_welcome.ts create mode 100644 x-pack/test/fleet_functional/apps/home/index.ts create mode 100644 x-pack/test/fleet_functional/apps/home/welcome.ts diff --git a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap index 4e66fd9e14c81..348f618805858 100644 --- a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap @@ -4,6 +4,7 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = `
{ defaultProps.localStorage.getItem = sinon.spy(() => 'true'); const component = await renderHome({ - find: () => Promise.resolve({ total: 0 }), + http: { + get: () => Promise.resolve({ isNewInstance: true }), + }, }); sinon.assert.calledOnce(defaultProps.localStorage.getItem); diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index cb02d62f9164f..da8eac6c78a8d 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -33,6 +33,7 @@ export function HomeApp({ directories, solutions }) { addBasePath, environmentService, telemetry, + http, } = getServices(); const environment = environmentService.getEnvironment(); const isCloudEnabled = environment.cloud; @@ -71,10 +72,10 @@ export function HomeApp({ directories, solutions }) { addBasePath={addBasePath} directories={directories} solutions={solutions} - find={savedObjectsClient.find} localStorage={localStorage} urlBasePath={getBasePath()} telemetry={telemetry} + http={http} /> diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index 55b733e413f6a..ca7e6874c75c2 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -119,7 +119,7 @@ export class Welcome extends React.Component { const { urlBasePath, telemetry } = this.props; return ( -
+
diff --git a/src/plugins/home/server/routes/fetch_new_instance_status.ts b/src/plugins/home/server/routes/fetch_new_instance_status.ts new file mode 100644 index 0000000000000..12d94feb3b8a1 --- /dev/null +++ b/src/plugins/home/server/routes/fetch_new_instance_status.ts @@ -0,0 +1,35 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { IRouter } from 'src/core/server'; +import { isNewInstance } from '../services/new_instance_status'; + +export const registerNewInstanceStatusRoute = (router: IRouter) => { + router.get( + { + path: '/internal/home/new_instance_status', + validate: false, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { client: soClient } = context.core.savedObjects; + const { client: esClient } = context.core.elasticsearch; + + try { + return res.ok({ + body: { + isNewInstance: await isNewInstance({ esClient, soClient }), + }, + }); + } catch (e) { + return res.customError({ + statusCode: 500, + }); + } + }) + ); +}; diff --git a/src/plugins/home/server/routes/index.ts b/src/plugins/home/server/routes/index.ts index 905304e059660..6013dbf130831 100644 --- a/src/plugins/home/server/routes/index.ts +++ b/src/plugins/home/server/routes/index.ts @@ -8,7 +8,9 @@ import { IRouter } from 'src/core/server'; import { registerHitsStatusRoute } from './fetch_es_hits_status'; +import { registerNewInstanceStatusRoute } from './fetch_new_instance_status'; export const registerRoutes = (router: IRouter) => { registerHitsStatusRoute(router); + registerNewInstanceStatusRoute(router); }; diff --git a/src/plugins/home/server/services/new_instance_status.test.ts b/src/plugins/home/server/services/new_instance_status.test.ts new file mode 100644 index 0000000000000..9ce8f8571f5a1 --- /dev/null +++ b/src/plugins/home/server/services/new_instance_status.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import { isNewInstance } from './new_instance_status'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '../../../../core/server/mocks'; + +describe('isNewInstance', () => { + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + const soClient = savedObjectsClientMock.create(); + + beforeEach(() => jest.resetAllMocks()); + + it('returns true when there are no index patterns', async () => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 0, + saved_objects: [], + }); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns false when there are any index patterns other than metrics-* or logs-*', async () => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: '1', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'my-pattern-*' }, + }, + ], + }); + expect(await isNewInstance({ esClient, soClient })).toEqual(false); + }); + + describe('when only metrics-* and logs-* index patterns exist', () => { + beforeEach(() => { + soClient.find.mockResolvedValue({ + page: 1, + per_page: 100, + total: 2, + saved_objects: [ + { + id: '1', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'metrics-*' }, + }, + { + id: '2', + references: [], + type: 'index-pattern', + score: 99, + attributes: { title: 'logs-*' }, + }, + ], + }); + }); + + it('calls /_cat/indices for the index patterns', async () => { + await isNewInstance({ esClient, soClient }); + expect(esClient.asCurrentUser.cat.indices).toHaveBeenCalledWith({ + index: 'logs-*,metrics-*', + format: 'json', + }); + }); + + it('returns true if no logs or metrics indices exist', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns true if no logs or metrics indices contain data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-metrics-foo', 'docs.count': '0' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns true if only metrics-elastic_agent index contains data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-metrics-elastic_agent', 'docs.count': '100' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns true if only logs-elastic_agent index contains data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-logs-elastic_agent', 'docs.count': '100' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(true); + }); + + it('returns false if any other logs or metrics indices contain data', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise([ + { index: '.ds-metrics-foo', 'docs.count': '100' }, + ]) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(false); + }); + + it('returns false if an authentication error is thrown', async () => { + esClient.asCurrentUser.cat.indices.mockReturnValue( + elasticsearchServiceMock.createErrorTransportRequestPromise({}) + ); + expect(await isNewInstance({ esClient, soClient })).toEqual(false); + }); + }); +}); diff --git a/src/plugins/home/server/services/new_instance_status.ts b/src/plugins/home/server/services/new_instance_status.ts new file mode 100644 index 0000000000000..00223589a8d41 --- /dev/null +++ b/src/plugins/home/server/services/new_instance_status.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import type { IScopedClusterClient, SavedObjectsClientContract } from '../../../../core/server'; +import type { IndexPatternSavedObjectAttrs } from '../../../data/common/index_patterns/index_patterns'; + +const LOGS_INDEX_PATTERN = 'logs-*'; +const METRICS_INDEX_PATTERN = 'metrics-*'; + +const INDEX_PREFIXES_TO_IGNORE = [ + '.ds-metrics-elastic_agent', // ignore index created by Fleet server itself + '.ds-logs-elastic_agent', // ignore index created by Fleet server itself +]; + +interface Deps { + esClient: IScopedClusterClient; + soClient: SavedObjectsClientContract; +} + +export const isNewInstance = async ({ esClient, soClient }: Deps): Promise => { + const indexPatterns = await soClient.find({ + type: 'index-pattern', + fields: ['title'], + search: `*`, + searchFields: ['title'], + perPage: 100, + }); + + // If there are no index patterns, assume this is a new instance + if (indexPatterns.total === 0) { + return true; + } + + // If there are any index patterns that are not the default metrics-* and logs-* ones created by Fleet, assume this + // is not a new instance + if ( + indexPatterns.saved_objects.some( + (ip) => + ip.attributes.title !== LOGS_INDEX_PATTERN && ip.attributes.title !== METRICS_INDEX_PATTERN + ) + ) { + return false; + } + + try { + const logsAndMetricsIndices = await esClient.asCurrentUser.cat.indices({ + index: `${LOGS_INDEX_PATTERN},${METRICS_INDEX_PATTERN}`, + format: 'json', + }); + + const anyIndicesContainerUserData = logsAndMetricsIndices.body + // Ignore some data that is shipped by default + .filter(({ index }) => !INDEX_PREFIXES_TO_IGNORE.some((prefix) => index?.startsWith(prefix))) + // If any other logs and metrics indices have data, return false + .some((catResult) => (catResult['docs.count'] ?? '0') !== '0'); + + return !anyIndicesContainerUserData; + } catch (e) { + // If any errors are encountered return false to be safe + return false; + } +}; diff --git a/test/common/config.js b/test/common/config.js index 5b5d01cfeb1e4..eb110fad55ea8 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -41,7 +41,6 @@ export default function () { )}`, `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, - `--home.disableWelcomeScreen=true`, // Needed for async search functional tests to introduce a delay `--data.search.aggs.shardDelay.enabled=true`, `--security.showInsecureClusterWarning=false`, diff --git a/test/functional/apps/home/_welcome.ts b/test/functional/apps/home/_welcome.ts new file mode 100644 index 0000000000000..ec7e9759558df --- /dev/null +++ b/test/functional/apps/home/_welcome.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'home']); + + describe('Welcome interstitial', () => { + before(async () => { + // Need to navigate to page first to clear storage before test can be run + await PageObjects.common.navigateToUrl('home', undefined); + await browser.clearLocalStorage(); + await esArchiver.emptyKibanaIndex(); + }); + + it('is displayed on a fresh on-prem install', async () => { + await PageObjects.common.navigateToUrl('home', undefined, { disableWelcomePrompt: false }); + expect(await PageObjects.home.isWelcomeInterstitialDisplayed()).to.be(true); + }); + }); +} diff --git a/test/functional/apps/home/index.js b/test/functional/apps/home/index.js index ff6e522e41639..257ee724f6c8b 100644 --- a/test/functional/apps/home/index.js +++ b/test/functional/apps/home/index.js @@ -21,5 +21,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_newsfeed')); loadTestFile(require.resolve('./_add_data')); loadTestFile(require.resolve('./_sample_data')); + loadTestFile(require.resolve('./_welcome')); }); } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 49d56d6f43784..70589b9d9505e 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -19,6 +19,7 @@ interface NavigateProps { shouldLoginIfPrompted: boolean; useActualUrl: boolean; insertTimestamp: boolean; + disableWelcomePrompt: boolean; } export class CommonPageObject extends FtrService { private readonly log = this.ctx.getService('log'); @@ -37,11 +38,17 @@ export class CommonPageObject extends FtrService { * Logins to Kibana as default user and navigates to provided app * @param appUrl Kibana URL */ - private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { + private async loginIfPrompted( + appUrl: string, + insertTimestamp: boolean, + disableWelcomePrompt: boolean + ) { // Disable the welcome screen. This is relevant for environments // which don't allow to use the yml setting, e.g. cloud production. // It is done here so it applies to logins but also to a login re-use. - await this.browser.setLocalStorageItem('home:welcome:show', 'false'); + if (disableWelcomePrompt) { + await this.browser.setLocalStorageItem('home:welcome:show', 'false'); + } let currentUrl = await this.browser.getCurrentUrl(); this.log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); @@ -76,6 +83,7 @@ export class CommonPageObject extends FtrService { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, + disableWelcomePrompt, useActualUrl, insertTimestamp, } = navigateProps; @@ -95,7 +103,7 @@ export class CommonPageObject extends FtrService { await alert?.accept(); const currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl, insertTimestamp) + ? await this.loginIfPrompted(appUrl, insertTimestamp, disableWelcomePrompt) : await this.browser.getCurrentUrl(); if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { @@ -117,6 +125,7 @@ export class CommonPageObject extends FtrService { basePath = '', ensureCurrentUrl = true, shouldLoginIfPrompted = true, + disableWelcomePrompt = true, useActualUrl = false, insertTimestamp = true, shouldUseHashForSubUrl = true, @@ -136,6 +145,7 @@ export class CommonPageObject extends FtrService { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, + disableWelcomePrompt, useActualUrl, insertTimestamp, }); @@ -156,6 +166,7 @@ export class CommonPageObject extends FtrService { basePath = '', ensureCurrentUrl = true, shouldLoginIfPrompted = true, + disableWelcomePrompt = true, useActualUrl = true, insertTimestamp = true, } = {} @@ -170,6 +181,7 @@ export class CommonPageObject extends FtrService { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, + disableWelcomePrompt, useActualUrl, insertTimestamp, }); @@ -202,7 +214,13 @@ export class CommonPageObject extends FtrService { async navigateToApp( appName: string, - { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} + { + basePath = '', + shouldLoginIfPrompted = true, + disableWelcomePrompt = true, + hash = '', + insertTimestamp = true, + } = {} ) { let appUrl: string; if (this.config.has(['apps', appName])) { @@ -233,7 +251,7 @@ export class CommonPageObject extends FtrService { this.log.debug('returned from get, calling refresh'); await this.browser.refresh(); let currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl, insertTimestamp) + ? await this.loginIfPrompted(appUrl, insertTimestamp, disableWelcomePrompt) : await this.browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index c318635fc8548..8929026a28122 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -30,6 +30,10 @@ export class HomePageObject extends FtrService { return !(await this.testSubjects.exists(`addSampleDataSet${id}`)); } + async isWelcomeInterstitialDisplayed() { + return await this.testSubjects.isDisplayed('homeWelcomeInterstitial'); + } + async getVisibileSolutions() { const solutionPanels = await this.testSubjects.findAll('~homSolutionPanel', 2000); const panelAttributes = await Promise.all( diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index d38203d5d07d3..73d92f8ff722b 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -469,6 +469,15 @@ class BrowserService extends FtrService { await this.driver.executeScript('return window.localStorage.removeItem(arguments[0]);', key); } + /** + * Clears all values in local storage for the focused window/frame. + * + * @return {Promise} + */ + public async clearLocalStorage(): Promise { + await this.driver.executeScript('return window.localStorage.clear();'); + } + /** * Clears session storage for the focused window/frame. * diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index e00526cbb4ec4..2568f40594f10 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -153,7 +153,7 @@ export async function handleInstallPackageFailure({ try { const installType = getInstallType({ pkgVersion, installedPkg }); if (installType === 'install' || installType === 'reinstall') { - logger.error(`uninstalling ${pkgkey} after error installing`); + logger.error(`uninstalling ${pkgkey} after error installing: [${error.toString()}]`); await removeInstallation({ savedObjectsClient, pkgkey, esClient }); } @@ -271,6 +271,7 @@ async function installPackageFromRegistry({ return { assets, status: 'installed', installType }; }) .catch(async (err: Error) => { + logger.warn(`Failure to install package [${pkgName}]: [${err.toString()}]`); await handleInstallPackageFailure({ savedObjectsClient, error: err, diff --git a/x-pack/test/fleet_functional/apps/home/index.ts b/x-pack/test/fleet_functional/apps/home/index.ts new file mode 100644 index 0000000000000..cd14bfdff557d --- /dev/null +++ b/x-pack/test/fleet_functional/apps/home/index.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { loadTestFile } = providerContext; + + describe('home onboarding', function () { + this.tags('ciGroup7'); + loadTestFile(require.resolve('./welcome')); + }); +} diff --git a/x-pack/test/fleet_functional/apps/home/welcome.ts b/x-pack/test/fleet_functional/apps/home/welcome.ts new file mode 100644 index 0000000000000..3a9a3a05e9226 --- /dev/null +++ b/x-pack/test/fleet_functional/apps/home/welcome.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'home']); + const kibanaServer = getService('kibanaServer'); + + describe('Welcome interstitial', () => { + before(async () => { + // Need to navigate to page first to clear storage before test can be run + await PageObjects.common.navigateToUrl('home', undefined); + await browser.clearLocalStorage(); + await esArchiver.emptyKibanaIndex(); + }); + + /** + * When we run this against a Cloud cluster, we also test the case where Fleet server is running + * and ingesting elastic_agent data. + */ + it('is displayed on a fresh install with Fleet setup executed', async () => { + // Setup Fleet and verify the metrics index pattern was created + await kibanaServer.request({ path: '/api/fleet/setup', method: 'POST' }); + const metricsIndexPattern = await kibanaServer.savedObjects.get({ + type: 'index-pattern', + id: 'metrics-*', + }); + expect(metricsIndexPattern?.attributes.title).to.eql('metrics-*'); + + // Reload the home screen and verify the interstitial is displayed + await PageObjects.common.navigateToUrl('home', undefined, { disableWelcomePrompt: false }); + expect(await PageObjects.home.isWelcomeInterstitialDisplayed()).to.be(true); + }); + + // Pending tests we should add once the FTR supports Elastic Agent / Fleet Server + it('is still displayed after a Fleet server is enrolled with agent metrics'); + it('is not displayed after an agent is enrolled with system metrics'); + it('is not displayed after a standalone agent is enrolled with system metrics'); + }); +} diff --git a/x-pack/test/fleet_functional/config.ts b/x-pack/test/fleet_functional/config.ts index 15d0c72ffc603..b68fd08b7890f 100644 --- a/x-pack/test/fleet_functional/config.ts +++ b/x-pack/test/fleet_functional/config.ts @@ -16,7 +16,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...xpackFunctionalConfig.getAll(), pageObjects, - testFiles: [resolve(__dirname, './apps/fleet')], + testFiles: [resolve(__dirname, './apps/fleet'), resolve(__dirname, './apps/home')], junit: { reportName: 'X-Pack Fleet Functional Tests', },