From 9ff5f7d5bbbafba360df2ea3b942eaed965f8b3e Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 8 Apr 2025 10:59:43 +0200 Subject: [PATCH 1/3] [ftr] split feature controls tests into v1/v2 configs --- .buildkite/ftr_platform_stateful_configs.yml | 1 + .../apps/saved_query_management/config.ts | 2 +- .../apps/saved_query_management/config.v2.ts | 17 + .../feature_controls/create_security_tests.ts | 295 ++++++++++++++++ .../feature_controls/security.ts | 319 ------------------ .../feature_controls/{ => v1}/index.ts | 3 +- .../feature_controls/v1/security.ts | 37 ++ .../feature_controls/v2/index.ts | 14 + .../feature_controls/v2/security.v2.ts | 37 ++ 9 files changed, 403 insertions(+), 322 deletions(-) create mode 100644 x-pack/test/functional/apps/saved_query_management/config.v2.ts create mode 100644 x-pack/test/functional/apps/saved_query_management/feature_controls/create_security_tests.ts delete mode 100644 x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts rename x-pack/test/functional/apps/saved_query_management/feature_controls/{ => v1}/index.ts (81%) create mode 100644 x-pack/test/functional/apps/saved_query_management/feature_controls/v1/security.ts create mode 100644 x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts create mode 100644 x-pack/test/functional/apps/saved_query_management/feature_controls/v2/security.v2.ts diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index ca0b0e55cea12..3a2e8881d973c 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -266,6 +266,7 @@ enabled: - x-pack/test/functional/apps/rollup_job/config.ts - x-pack/test/functional/apps/saved_objects_management/config.ts - x-pack/test/functional/apps/saved_query_management/config.ts + - x-pack/test/functional/apps/saved_query_management/config.v2.ts - x-pack/test/functional/apps/security/config.ts - x-pack/test/functional/apps/snapshot_restore/config.ts - x-pack/test/functional/apps/spaces/config.ts diff --git a/x-pack/test/functional/apps/saved_query_management/config.ts b/x-pack/test/functional/apps/saved_query_management/config.ts index d0d07ff200281..55ae51d6c5c2e 100644 --- a/x-pack/test/functional/apps/saved_query_management/config.ts +++ b/x-pack/test/functional/apps/saved_query_management/config.ts @@ -12,6 +12,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), - testFiles: [require.resolve('.')], + testFiles: [require.resolve('./feature_controls/v1')], }; } diff --git a/x-pack/test/functional/apps/saved_query_management/config.v2.ts b/x-pack/test/functional/apps/saved_query_management/config.v2.ts new file mode 100644 index 0000000000000..79d91cc620607 --- /dev/null +++ b/x-pack/test/functional/apps/saved_query_management/config.v2.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('./feature_controls/v2')], + }; +} diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/create_security_tests.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/create_security_tests.ts new file mode 100644 index 0000000000000..3a498263fd393 --- /dev/null +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/create_security_tests.ts @@ -0,0 +1,295 @@ +/* + * 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'; +import { getSavedQuerySecurityUtils } from '../utils/saved_query_security'; + +export type FeatureName = + | 'discover' + | 'discover_v2' + | 'dashboard' + | 'dashboard_v2' + | 'maps' + | 'maps_v2' + | 'visualize' + | 'visualize_v2'; +export type FeatureApp = 'discover' | 'dashboard' | 'maps' | 'visualize'; + +export function createSecurityTests( + featureConfigs: Array<{ + feature: FeatureName; + app: FeatureApp; + hasImplicitSaveQueryManagement: boolean; + }> +) { + return function (ctx: FtrProviderContext) { + const { getPageObjects, getService } = ctx; + const savedQuerySecurityUtils = getSavedQuerySecurityUtils(ctx); + const esArchiver = getService('esArchiver'); + const securityService = getService('security'); + const globalNav = getService('globalNav'); + const { common, discover, security, dashboard, maps, visualize, spaceSelector } = + getPageObjects([ + 'common', + 'discover', + 'security', + 'dashboard', + 'maps', + 'visualize', + 'spaceSelector', + ]); + const kibanaServer = getService('kibanaServer'); + + async function login( + featureName: FeatureName, + featurePrivilege: 'read' | 'all', + globalPrivilege: 'none' | 'read' | 'all', + expectSpaceSelector = false + ) { + const name = `global_saved_query_${featureName}`; + const password = `password_${name}_${featurePrivilege}_${globalPrivilege}`; + + await securityService.role.create(name, { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + [featureName]: [featurePrivilege], + savedQueryManagement: [globalPrivilege], + }, + spaces: ['*'], + }, + ], + }); + + await securityService.user.create(`${name}-user`, { + password, + roles: [name], + full_name: 'test user', + }); + + await security.login(`${name}-user`, password, { expectSpaceSelector }); + } + + async function logout(featureName: FeatureName) { + const name = `global_saved_query_${featureName}`; + await security.forceLogout(); + await securityService.role.delete(name); + await securityService.user.delete(`${name}-user`); + } + + async function navigateToApp(appName: FeatureApp) { + switch (appName) { + case 'discover': + await common.navigateToApp('discover'); + await discover.selectIndexPattern('logstash-*'); + break; + case 'dashboard': + await dashboard.navigateToApp(); + await dashboard.loadSavedDashboard('A Dashboard'); + break; + case 'maps': + await maps.openNewMap(); + break; + case 'visualize': + await visualize.navigateToNewVisualization(); + await visualize.clickVisType('lens'); + break; + default: + break; + } + } + + describe('Security', () => { + describe('App vs Global privilege', () => { + featureConfigs.forEach(({ feature, app, hasImplicitSaveQueryManagement }) => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json' + ); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + + // ensure we're logged out, so we can log in as the appropriate users + await security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await security.forceLogout(); + + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json' + ); + + await kibanaServer.savedObjects.cleanStandardList(); + }); + + describe(`${feature} read-only privileges with savedQueryManagement.saveQuery all privilege`, () => { + before(async () => { + await login(feature, 'read', 'all'); + await navigateToApp(app); + await common.waitForTopNavToBeVisible(); + }); + + after(async () => { + await logout(feature); + }); + + it('shows read-only badge', async () => { + await globalNav.badgeExistsOrFail('Read only'); + }); + + savedQuerySecurityUtils.shouldAllowSavingQueries(); + }); + + describe(`${feature} read-only privileges with savedQueryManagement.saveQuery read privilege`, () => { + before(async () => { + await login(feature, 'read', 'read'); + await navigateToApp(app); + await common.waitForTopNavToBeVisible(); + }); + + after(async () => { + await logout(feature); + }); + + it('shows read-only badge', async () => { + await globalNav.badgeExistsOrFail('Read only'); + }); + + savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries(); + }); + + describe(`${feature} read-only privileges with disabled savedQueryManagement.saveQuery privilege`, () => { + before(async () => { + await login(feature, 'read', 'none'); + await navigateToApp(app); + }); + + after(async () => { + await logout(feature); + }); + + it('shows read-only badge', async () => { + await globalNav.badgeExistsOrFail('Read only'); + }); + + if (hasImplicitSaveQueryManagement) { + savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries(); + } else { + savedQuerySecurityUtils.shouldDisallowAccessToSavedQueries(); + } + }); + + describe(`${feature} all privileges with savedQueryManagement.saveQuery all privilege`, () => { + before(async () => { + await login(feature, 'all', 'all'); + await navigateToApp(app); + }); + + after(async () => { + await logout(feature); + }); + + it("doesn't show read-only badge", async () => { + await globalNav.badgeMissingOrFail(); + }); + + savedQuerySecurityUtils.shouldAllowSavingQueries(); + }); + + describe(`${feature} all privileges with savedQueryManagement.saveQuery read privilege`, () => { + before(async () => { + await login(feature, 'all', 'read'); + await navigateToApp(app); + }); + + after(async () => { + await logout(feature); + }); + + it("doesn't show read-only badge", async () => { + await globalNav.badgeMissingOrFail(); + }); + + if (hasImplicitSaveQueryManagement) { + savedQuerySecurityUtils.shouldAllowSavingQueries(); + } else { + savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries(); + } + }); + + describe(`${feature} all privileges with disabled savedQueryManagement.saveQuery privilege`, () => { + before(async () => { + await login(feature, 'all', 'none'); + await navigateToApp(app); + }); + + after(async () => { + await logout(feature); + }); + + it("doesn't show read-only badge", async () => { + await globalNav.badgeMissingOrFail(); + }); + + if (hasImplicitSaveQueryManagement) { + savedQuerySecurityUtils.shouldAllowSavingQueries(); + } else { + savedQuerySecurityUtils.shouldDisallowAccessToSavedQueries(); + } + }); + }); + }); + + describe('Spaces feature visibility', () => { + featureConfigs.forEach(({ feature }) => { + describe(`space with ${feature} disabled`, () => { + const spaceId = `${feature}_space`; + let disabledFeatureId: string; + + before(async () => { + await kibanaServer.spaces.create({ + id: spaceId, + name: spaceId, + disabledFeatures: [feature], + }); + const disabledFeature = (await kibanaServer.spaces.get(spaceId)) as { + disabledFeatures: string[]; + }; + [disabledFeatureId] = disabledFeature.disabledFeatures; + await common.navigateToApp('home'); + }); + + after(async () => { + await kibanaServer.spaces.delete(spaceId); + }); + + it('should not disable saved query management feature visibility', async () => { + await spaceSelector.openSpacesNav(); + await spaceSelector.clickManageSpaces(); + await spaceSelector.clickSpaceEditButton(spaceId); + await spaceSelector.toggleFeatureCategoryVisibility('kibana'); + await spaceSelector.toggleFeatureCategoryVisibility('management'); + expect(await spaceSelector.getFeatureCheckboxState(disabledFeatureId)).to.be(false); + expect(await spaceSelector.getFeatureCheckboxState('savedQueryManagement')).to.be( + true + ); + }); + }); + }); + }); + }); + }; +} diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts deleted file mode 100644 index eaab1313fc0df..0000000000000 --- a/x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { getSavedQuerySecurityUtils } from '../utils/saved_query_security'; - -const featureConfigs = [ - { - feature: 'discover', - app: 'discover', - hasImplicitSaveQueryManagement: true, - }, - { - feature: 'dashboard', - app: 'dashboard', - hasImplicitSaveQueryManagement: true, - }, - { - feature: 'maps', - app: 'maps', - hasImplicitSaveQueryManagement: true, - }, - { - feature: 'visualize', - app: 'visualize', - hasImplicitSaveQueryManagement: true, - }, - { - feature: 'discover_v2', - app: 'discover', - hasImplicitSaveQueryManagement: false, - }, - { - feature: 'dashboard_v2', - app: 'dashboard', - hasImplicitSaveQueryManagement: false, - }, - { - feature: 'maps_v2', - app: 'maps', - hasImplicitSaveQueryManagement: false, - }, - { - feature: 'visualize_v2', - app: 'visualize', - hasImplicitSaveQueryManagement: false, - }, -] as const; - -type FeatureName = (typeof featureConfigs)[number]['feature']; -type FeatureApp = (typeof featureConfigs)[number]['app']; - -export default function (ctx: FtrProviderContext) { - const { getPageObjects, getService } = ctx; - const savedQuerySecurityUtils = getSavedQuerySecurityUtils(ctx); - const esArchiver = getService('esArchiver'); - const securityService = getService('security'); - const globalNav = getService('globalNav'); - const { common, discover, security, dashboard, maps, visualize, spaceSelector } = getPageObjects([ - 'common', - 'discover', - 'security', - 'dashboard', - 'maps', - 'visualize', - 'spaceSelector', - ]); - const kibanaServer = getService('kibanaServer'); - - async function login( - featureName: FeatureName, - featurePrivilege: 'read' | 'all', - globalPrivilege: 'none' | 'read' | 'all', - expectSpaceSelector = false - ) { - const name = `global_saved_query_${featureName}`; - const password = `password_${name}_${featurePrivilege}_${globalPrivilege}`; - - await securityService.role.create(name, { - elasticsearch: { - indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], - }, - kibana: [ - { - feature: { - [featureName]: [featurePrivilege], - savedQueryManagement: [globalPrivilege], - }, - spaces: ['*'], - }, - ], - }); - - await securityService.user.create(`${name}-user`, { - password, - roles: [name], - full_name: 'test user', - }); - - await security.login(`${name}-user`, password, { expectSpaceSelector }); - } - - async function logout(featureName: FeatureName) { - const name = `global_saved_query_${featureName}`; - await security.forceLogout(); - await securityService.role.delete(name); - await securityService.user.delete(`${name}-user`); - } - - async function navigateToApp(appName: FeatureApp) { - switch (appName) { - case 'discover': - await common.navigateToApp('discover'); - await discover.selectIndexPattern('logstash-*'); - break; - case 'dashboard': - await dashboard.navigateToApp(); - await dashboard.loadSavedDashboard('A Dashboard'); - break; - case 'maps': - await maps.openNewMap(); - break; - case 'visualize': - await visualize.navigateToNewVisualization(); - await visualize.clickVisType('lens'); - break; - default: - break; - } - } - - describe('Security', () => { - describe('App vs Global privilege', () => { - featureConfigs.forEach(({ feature, app, hasImplicitSaveQueryManagement }) => { - before(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - - await kibanaServer.importExport.load( - 'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json' - ); - - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); - - // ensure we're logged out, so we can log in as the appropriate users - await security.forceLogout(); - }); - - after(async () => { - // logout, so the other tests don't accidentally run as the custom users we're testing below - // NOTE: Logout needs to happen before anything else to avoid flaky behavior - await security.forceLogout(); - - await kibanaServer.importExport.unload( - 'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json' - ); - - await kibanaServer.savedObjects.cleanStandardList(); - }); - - describe(`${feature} read-only privileges with savedQueryManagement.saveQuery all privilege`, () => { - before(async () => { - await login(feature, 'read', 'all'); - await navigateToApp(app); - await common.waitForTopNavToBeVisible(); - }); - - after(async () => { - await logout(feature); - }); - - it('shows read-only badge', async () => { - await globalNav.badgeExistsOrFail('Read only'); - }); - - savedQuerySecurityUtils.shouldAllowSavingQueries(); - }); - - describe(`${feature} read-only privileges with savedQueryManagement.saveQuery read privilege`, () => { - before(async () => { - await login(feature, 'read', 'read'); - await navigateToApp(app); - await common.waitForTopNavToBeVisible(); - }); - - after(async () => { - await logout(feature); - }); - - it('shows read-only badge', async () => { - await globalNav.badgeExistsOrFail('Read only'); - }); - - savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries(); - }); - - describe(`${feature} read-only privileges with disabled savedQueryManagement.saveQuery privilege`, () => { - before(async () => { - await login(feature, 'read', 'none'); - await navigateToApp(app); - }); - - after(async () => { - await logout(feature); - }); - - it('shows read-only badge', async () => { - await globalNav.badgeExistsOrFail('Read only'); - }); - - if (hasImplicitSaveQueryManagement) { - savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries(); - } else { - savedQuerySecurityUtils.shouldDisallowAccessToSavedQueries(); - } - }); - - describe(`${feature} all privileges with savedQueryManagement.saveQuery all privilege`, () => { - before(async () => { - await login(feature, 'all', 'all'); - await navigateToApp(app); - }); - - after(async () => { - await logout(feature); - }); - - it("doesn't show read-only badge", async () => { - await globalNav.badgeMissingOrFail(); - }); - - savedQuerySecurityUtils.shouldAllowSavingQueries(); - }); - - describe(`${feature} all privileges with savedQueryManagement.saveQuery read privilege`, () => { - before(async () => { - await login(feature, 'all', 'read'); - await navigateToApp(app); - }); - - after(async () => { - await logout(feature); - }); - - it("doesn't show read-only badge", async () => { - await globalNav.badgeMissingOrFail(); - }); - - if (hasImplicitSaveQueryManagement) { - savedQuerySecurityUtils.shouldAllowSavingQueries(); - } else { - savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries(); - } - }); - - describe(`${feature} all privileges with disabled savedQueryManagement.saveQuery privilege`, () => { - before(async () => { - await login(feature, 'all', 'none'); - await navigateToApp(app); - }); - - after(async () => { - await logout(feature); - }); - - it("doesn't show read-only badge", async () => { - await globalNav.badgeMissingOrFail(); - }); - - if (hasImplicitSaveQueryManagement) { - savedQuerySecurityUtils.shouldAllowSavingQueries(); - } else { - savedQuerySecurityUtils.shouldDisallowAccessToSavedQueries(); - } - }); - }); - }); - - describe('Spaces feature visibility', () => { - featureConfigs.forEach(({ feature }) => { - describe(`space with ${feature} disabled`, () => { - const spaceId = `${feature}_space`; - let disabledFeatureId: string; - - before(async () => { - await kibanaServer.spaces.create({ - id: spaceId, - name: spaceId, - disabledFeatures: [feature], - }); - const disabledFeature = (await kibanaServer.spaces.get(spaceId)) as { - disabledFeatures: string[]; - }; - [disabledFeatureId] = disabledFeature.disabledFeatures; - await common.navigateToApp('home'); - }); - - after(async () => { - await kibanaServer.spaces.delete(spaceId); - }); - - it('should not disable saved query management feature visibility', async () => { - await spaceSelector.openSpacesNav(); - await spaceSelector.clickManageSpaces(); - await spaceSelector.clickSpaceEditButton(spaceId); - await spaceSelector.toggleFeatureCategoryVisibility('kibana'); - await spaceSelector.toggleFeatureCategoryVisibility('management'); - expect(await spaceSelector.getFeatureCheckboxState(disabledFeatureId)).to.be(false); - expect(await spaceSelector.getFeatureCheckboxState('savedQueryManagement')).to.be(true); - }); - }); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/index.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/index.ts similarity index 81% rename from x-pack/test/functional/apps/saved_query_management/feature_controls/index.ts rename to x-pack/test/functional/apps/saved_query_management/feature_controls/v1/index.ts index 4c7c03dd08334..859d3a00d1df0 100644 --- a/x-pack/test/functional/apps/saved_query_management/feature_controls/index.ts +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/index.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Feature controls', function () { - this.tags('skipFirefox'); loadTestFile(require.resolve('./security')); }); } diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/security.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/security.ts new file mode 100644 index 0000000000000..32355c8f9d5e4 --- /dev/null +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/security.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FeatureApp, FeatureName, createSecurityTests } from '../create_security_tests'; + +const featureConfigs: Array<{ + feature: FeatureName; + app: FeatureApp; + hasImplicitSaveQueryManagement: boolean; +}> = [ + { + feature: 'discover', + app: 'discover', + hasImplicitSaveQueryManagement: true, + }, + { + feature: 'dashboard', + app: 'dashboard', + hasImplicitSaveQueryManagement: true, + }, + { + feature: 'maps', + app: 'maps', + hasImplicitSaveQueryManagement: true, + }, + { + feature: 'visualize', + app: 'visualize', + hasImplicitSaveQueryManagement: true, + }, +]; + +export default createSecurityTests(featureConfigs); diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts new file mode 100644 index 0000000000000..916853e1c4591 --- /dev/null +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Feature controls - v2', function () { + loadTestFile(require.resolve('./security.v2.ts')); + }); +} diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/security.v2.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/security.v2.ts new file mode 100644 index 0000000000000..d5a92144a6607 --- /dev/null +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/security.v2.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FeatureApp, FeatureName, createSecurityTests } from '../create_security_tests'; + +const featureConfigs: Array<{ + feature: FeatureName; + app: FeatureApp; + hasImplicitSaveQueryManagement: boolean; +}> = [ + { + feature: 'discover_v2', + app: 'discover', + hasImplicitSaveQueryManagement: false, + }, + { + feature: 'dashboard_v2', + app: 'dashboard', + hasImplicitSaveQueryManagement: false, + }, + { + feature: 'maps_v2', + app: 'maps', + hasImplicitSaveQueryManagement: false, + }, + { + feature: 'visualize_v2', + app: 'visualize', + hasImplicitSaveQueryManagement: false, + }, +]; + +export default createSecurityTests(featureConfigs); From ba4256ddce8571863dc75e1b45dced2a9f886037 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 8 Apr 2025 11:33:40 +0200 Subject: [PATCH 2/3] fix paths --- .../functional/apps/discover/group1/config.ts | 17 + .../group1}/index.ts | 5 +- .../apps/discover/group1/reporting.ts | 403 ++++++++++++++++++ .../discover/group1/reporting_embeddable.ts | 178 ++++++++ .../feature_controls/v1/index.ts | 2 +- .../feature_controls/v2/index.ts | 2 +- 6 files changed, 603 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/functional/apps/discover/group1/config.ts rename x-pack/test/functional/apps/{saved_query_management => discover/group1}/index.ts (69%) create mode 100644 x-pack/test/functional/apps/discover/group1/reporting.ts create mode 100644 x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts diff --git a/x-pack/test/functional/apps/discover/group1/config.ts b/x-pack/test/functional/apps/discover/group1/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/discover/group1/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/saved_query_management/index.ts b/x-pack/test/functional/apps/discover/group1/index.ts similarity index 69% rename from x-pack/test/functional/apps/saved_query_management/index.ts rename to x-pack/test/functional/apps/discover/group1/index.ts index fb74e8ba554c4..d0319d028422d 100644 --- a/x-pack/test/functional/apps/saved_query_management/index.ts +++ b/x-pack/test/functional/apps/discover/group1/index.ts @@ -8,7 +8,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('Saved query management', function () { - loadTestFile(require.resolve('./feature_controls')); + describe('discover - group 1', function () { + loadTestFile(require.resolve('./reporting')); // 6 min + loadTestFile(require.resolve('./reporting_embeddable')); // 8 min }); } diff --git a/x-pack/test/functional/apps/discover/group1/reporting.ts b/x-pack/test/functional/apps/discover/group1/reporting.ts new file mode 100644 index 0000000000000..bb71966f5da64 --- /dev/null +++ b/x-pack/test/functional/apps/discover/group1/reporting.ts @@ -0,0 +1,403 @@ +/* + * 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 moment, { DurationInputArg2 } from 'moment'; +import { Key } from 'selenium-webdriver'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const reportingAPI = getService('reporting'); + const log = getService('log'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); + const retry = getService('retry'); + const { reporting, common, discover, timePicker, share, header } = getPageObjects([ + 'reporting', + 'common', + 'discover', + 'timePicker', + 'share', + 'header', + ]); + const monacoEditor = getService('monacoEditor'); + const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); + + const deleteIndex = async (index: string) => { + try { + await es.indices.delete({ index }); + } catch (err) { + // ignore 404 error + } + }; + + const createDocs = async ({ + index, + endDate, + docCount, + dateSubstractUnit, + addNumberField, + }: { + index: string; + endDate: string; + docCount: number; + dateSubstractUnit?: DurationInputArg2; + addNumberField?: boolean; + }) => { + interface TestDoc { + timestamp: string; + name: string; + updated_at?: string; + numberValue?: number; + } + + const docs = Array(docCount); + + for (let i = 0; i <= docs.length - 1; i++) { + const name = `test-${i + 1}`; + const timestamp = moment + .utc(endDate) + .subtract(docCount - i, dateSubstractUnit ?? 'days') + .format(); + + const commonFields: Pick = { + timestamp, + name, + }; + + if (addNumberField) { + commonFields.numberValue = i; + } + + if (i === 0) { + // only the oldest document has a value for updated_at + docs[i] = { + ...commonFields, + updated_at: moment.utc(endDate).format(), + }; + } else { + // updated_at field does not exist in first 500 documents + docs[i] = commonFields; + } + } + + const res = await es.bulk({ + index, + operations: docs.map((d) => `{"index": {}}\n${JSON.stringify(d)}\n`), + }); + + log.info(`Indexed ${res.items.length} test data docs into ${index}.`); + }; + + const getReport = async ({ timeout } = { timeout: 60 * 1000 }) => { + // close any open notification toasts + await toasts.dismissAll(); + + await reporting.openExportTab(); + await reporting.clickGenerateReportButton(); + + const url = await reporting.getReportURL(timeout); + const res = await reporting.getResponse(url ?? ''); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + return res; + }; + + const getReportPostUrl = async () => { + // click 'Copy POST URL' + await share.clickShareTopNavButton(); + await reporting.openExportTab(); + const copyButton = await testSubjects.find('shareReportingCopyURL'); + + return decodeURIComponent((await copyButton.getAttribute('data-share-url')) ?? ''); + }; + + describe('Discover CSV Export', () => { + describe('Check Available', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + await reportingAPI.initEcommerce(); + await common.navigateToApp('discover'); + await discover.selectIndexPattern('ecommerce'); + }); + + after(async () => { + await reportingAPI.teardownEcommerce(); + await esArchiver.emptyKibanaIndex(); + }); + + it('is available if new', async () => { + await reporting.openExportTab(); + expect(await reporting.isGenerateReportButtonDisabled()).to.be(null); + await share.closeShareModal(); + }); + + it('becomes available when saved', async () => { + await discover.saveSearch('my search - expectEnabledGenerateReportButton'); + await reporting.openExportTab(); + expect(await reporting.isGenerateReportButtonDisabled()).to.be(null); + await share.closeShareModal(); + }); + }); + + describe('Generate CSV: new search', () => { + before(async () => { + await reportingAPI.initEcommerce(); + /** + * Important: `esArchiver.emptyKibanaIndex()` above also resets the + * Kibana time zone setting, so we're re-applying it here. + * The serverless version of the test uses + * `kibanaServer.savedObjects.cleanStandardList` instead, + * which does not reset the time zone setting, + * so we don't need to re-apply it in these tests. + */ + await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'UTC' }); + }); + + after(async () => { + await reportingAPI.teardownEcommerce(); + await esArchiver.emptyKibanaIndex(); + }); + + beforeEach(async () => { + await common.navigateToApp('discover'); + await discover.selectIndexPattern('ecommerce'); + }); + + it('generates a report with single timefilter', async () => { + await discover.clickNewSearchButton(); + await timePicker.setCommonlyUsedTime('Last_24 hours'); + await discover.saveSearch('single-timefilter-search'); + + // get shared URL value + const sharedURL = await browser.getCurrentUrl(); + + // click 'Copy POST URL' + await share.clickShareTopNavButton(); + await reporting.openExportTab(); + const copyButton = await testSubjects.find('shareReportingCopyURL'); + const reportURL = decodeURIComponent( + (await copyButton.getAttribute('data-share-url')) ?? '' + ); + + // get number of filters in URLs + const timeFiltersNumberInReportURL = + reportURL.split('query:(range:(order_date:(format:strict_date_optional_time').length - 1; + const timeFiltersNumberInSharedURL = sharedURL.split('time:').length - 1; + + expect(timeFiltersNumberInSharedURL).to.be(1); + expect(sharedURL.includes('time:(from:now-24h%2Fh,to:now))')).to.be(true); + + expect(timeFiltersNumberInReportURL).to.be(1); + + expect( + reportURL.includes( + `query:(range:(order_date:(format:strict_date_optional_time,gte:now-24h/h,lte:now))))` + ) + ).to.be(true); + + // return keyboard state + await browser.getActions().keyUp(Key.CONTROL).perform(); + await browser.getActions().keyUp('v').perform(); + }); + + it('generates a report from a new search with data: default', async () => { + await discover.clickNewSearchButton(); + await reporting.setTimepickerInEcommerceDataRange(); + + await discover.saveSearch('my search - with data - expectReportCanBeCreated'); + + const res = await getReport(); + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + + const csvFile = res.text; + expectSnapshot(csvFile).toMatch(); + }); + + it('generates a report with no data', async () => { + await reporting.setTimepickerInEcommerceNoDataRange(); + await discover.saveSearch('my search - no data - expectReportCanBeCreated'); + + const res = await getReport(); + expect(res.text).to.be(`\n`); + }); + + it('generates a large export', async () => { + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await timePicker.setAbsoluteRange(fromTime, toTime); + await discover.clickNewSearchButton(); + await retry.try(async () => { + expect(await discover.getHitCount()).to.equal('4,675'); + }); + await discover.saveSearch('large export'); + + // match file length, the beginning and the end of the csv file contents + const { text: csvFile } = await getReport({ timeout: 80 * 1000 }); + expect(csvFile.length).to.be(4845684); + expectSnapshot(csvFile.slice(0, 5000)).toMatch(); + expectSnapshot(csvFile.slice(-5000)).toMatch(); + }); + + it('generate a report using ES|QL', async () => { + await discover.selectTextBaseLang(); + const testQuery = `from ecommerce | STATS total_sales = SUM(taxful_total_price) BY day_of_week | SORT total_sales DESC`; + + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await header.waitUntilLoadingHasFinished(); + + const res = await getReport(); + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + + const csvFile = res.text; + expectSnapshot(csvFile).toMatch(); + }); + + it('generate a report using ES|QL for relative time range as absolute dates and time params', async () => { + const RECENT_DATA_INDEX_NAME = 'test_recent_data'; + const RECENT_DOC_COUNT = 500; + const RECENT_DOC_END_DATE = moment().toISOString(); + + await deleteIndex(RECENT_DATA_INDEX_NAME); + await createDocs({ + index: RECENT_DATA_INDEX_NAME, + endDate: RECENT_DOC_END_DATE, + docCount: RECENT_DOC_COUNT, + dateSubstractUnit: 'minutes', + addNumberField: true, + }); + + await timePicker.setCommonlyUsedTime('Last_15 minutes'); + await discover.selectTextBaseLang(); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + const testQuery = `from ${RECENT_DATA_INDEX_NAME} | sort timestamp | WHERE timestamp >= ?_tstart AND timestamp <= ?_tend | KEEP name, numberValue`; + await monacoEditor.setCodeEditorValue(testQuery); + await testSubjects.click('querySubmitButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + + const reportPostUrl = await getReportPostUrl(); + expect(reportPostUrl).to.contain(`timeRange:(from:'2`); // not `from:now-15m` + expect(reportPostUrl).to.contain(`filters:!()`); + expect(reportPostUrl).to.contain(`query:(esql:'${testQuery}')`); + + const res = await getReport(); + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + + const csvFile = res.text; + expectSnapshot(csvFile).toMatch(); + + await deleteIndex(RECENT_DATA_INDEX_NAME); + }); + }); + + describe('Generate CSV: sparse data', () => { + const TEST_INDEX_NAME = 'sparse_data'; + const TEST_DOC_COUNT = 510; + const TEST_DOC_END_DATE = '2006-08-14T00:00:00'; + + before(async () => { + await deleteIndex(TEST_INDEX_NAME); + await createDocs({ + index: TEST_INDEX_NAME, + endDate: TEST_DOC_END_DATE, + docCount: TEST_DOC_COUNT, + dateSubstractUnit: 'days', + }); + await reportingAPI.initLogs(); + await common.navigateToApp('discover'); + await discover.loadSavedSearch('Sparse Columns'); + }); + + after(async () => { + await reportingAPI.teardownLogs(); + await deleteIndex(TEST_INDEX_NAME); + }); + + beforeEach(async () => { + const fromTime = 'Jan 10, 2005 @ 00:00:00.000'; + const toTime = 'Dec 23, 2006 @ 00:00:00.000'; + await timePicker.setAbsoluteRange(fromTime, toTime); + await retry.try(async () => { + expect(await discover.getHitCount()).to.equal(TEST_DOC_COUNT.toString()); + }); + }); + + it(`handles field formatting for a field that doesn't exist initially`, async () => { + const res = await getReport(); + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + + const csvFile = res.text; + expectSnapshot(csvFile).toMatch(); + }); + }); + + describe('Generate CSV: archived search', () => { + const setupPage = async () => { + const fromTime = 'Jun 22, 2019 @ 00:00:00.000'; + const toTime = 'Jun 26, 2019 @ 23:30:00.000'; + await timePicker.setAbsoluteRange(fromTime, toTime); + }; + + before(async () => { + await reportingAPI.initEcommerce(); + await common.navigateToApp('discover'); + await discover.selectIndexPattern('ecommerce'); + }); + + after(async () => { + await reportingAPI.teardownEcommerce(); + }); + + beforeEach(async () => { + await setupPage(); + }); + + afterEach(async () => { + await reporting.checkForReportingToasts(); + }); + + it('generates a report with data', async () => { + await discover.loadSavedSearch('Ecommerce Data'); + await retry.try(async () => { + expect(await discover.getHitCount()).to.equal('740'); + }); + + const { text: csvFile } = await getReport(); + expectSnapshot(csvFile).toMatch(); + }); + + it('generates a report with filtered data', async () => { + await discover.loadSavedSearch('Ecommerce Data'); + await retry.try(async () => { + expect(await discover.getHitCount()).to.equal('740'); + }); + + // filter + await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` }); + await retry.try(async () => { + expect(await discover.getHitCount()).to.equal('154'); + }); + + const { text: csvFile } = await getReport(); + expectSnapshot(csvFile).toMatch(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts b/x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts new file mode 100644 index 0000000000000..0416ba483d4b3 --- /dev/null +++ b/x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts @@ -0,0 +1,178 @@ +/* + * 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 esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const retry = getService('retry'); + const { reporting, common, discover, timePicker, header, dashboard, unifiedFieldList } = + getPageObjects([ + 'reporting', + 'common', + 'discover', + 'timePicker', + 'header', + 'dashboard', + 'unifiedFieldList', + ]); + const monacoEditor = getService('monacoEditor'); + const queryBar = getService('queryBar'); + const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); + const dataGrid = getService('dataGrid'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardCustomizePanel = getService('dashboardCustomizePanel'); + const dashboardBadgeActions = getService('dashboardBadgeActions'); + + const GENERATE_CSV_DATA_TEST_SUBJ = 'embeddablePanelAction-generateCsvReport'; + const SAVED_DISCOVER_SESSION_WITH_DATA_VIEW = 'savedDiscoverSessionWithDataView'; + const SAVED_DISCOVER_SESSION_WITH_ESQL = 'savedDiscoverSessionWithESQL'; + + const getDashboardPanelReport = async (title: string, { timeout } = { timeout: 60 * 1000 }) => { + await toasts.dismissAll(); + + await dashboardPanelActions.expectExistsPanelAction(GENERATE_CSV_DATA_TEST_SUBJ, title); + await dashboardPanelActions.clickPanelActionByTitle(GENERATE_CSV_DATA_TEST_SUBJ, title); + await testSubjects.existOrFail('csvReportStarted'); /* validate toast panel */ + + const url = await reporting.getReportURL(timeout); + const res = await reporting.getResponse(url ?? ''); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + return res; + }; + + const addEmbeddableToDashboard = async (title: string) => { + await dashboardAddPanel.addSavedSearch(title); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + const rows = await dataGrid.getDocTableRows(); + expect(rows.length).to.be.above(0); + await dashboard.saveDashboard('test-csv', { + saveAsNew: true, + waitDialogIsClosed: false, + exitFromEditMode: true, + }); + await header.waitUntilLoadingHasFinished(); + await dashboard.waitForRenderComplete(); + }; + + const setDashboardGlobalTimeRange = async () => { + const fromTime = 'Sep 19, 2015 @ 16:31:44.000'; + const toTime = 'Sep 21, 2015 @ 11:31:44.000'; + await timePicker.setAbsoluteRange(fromTime, toTime); + }; + + const setDashboardPanelCustomTimeRange = async () => { + await dashboardPanelActions.customizePanel(); + await dashboardCustomizePanel.enableCustomTimeRange(); + + const toTime = 'Sep 21, 2015 @ 01:31:44.000'; + const endTimeTestSubj = + 'customizePanelTimeRangeDatePicker > superDatePickerendDatePopoverButton'; + await retry.waitFor(`endDate is set to ${toTime}`, async () => { + await testSubjects.click(endTimeTestSubj); + await testSubjects.click('superDatePickerAbsoluteDateInput'); + await timePicker.inputValue('superDatePickerAbsoluteDateInput', toTime); + await testSubjects.click(endTimeTestSubj); + const actualToTime = await testSubjects.getVisibleText(endTimeTestSubj); + return toTime === actualToTime; + }); + + await dashboardCustomizePanel.clickSaveButton(); + await dashboard.waitForRenderComplete(); + await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); + }; + + describe('Discover Embeddable - Generate CSV report per panel', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'src/platform/test/functional/fixtures/kbn_archiver/discover' + ); + await timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await common.navigateToApp('discover'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await discover.selectIndexPattern('logstash-*'); + + // create and save a discover session with filters in data view mode + await unifiedFieldList.waitUntilSidebarHasLoaded(); + await unifiedFieldList.clickFieldListItem('geo.src'); + await unifiedFieldList.clickFieldListPlusFilter('geo.src', 'US'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await unifiedFieldList.clickFieldListItemAdd('extension'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await queryBar.setQuery('machine.os : ios'); + await queryBar.submitQuery(); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + expect(await dataGrid.getDocCount()).to.be(222); + await discover.saveSearch(SAVED_DISCOVER_SESSION_WITH_DATA_VIEW); + await header.waitUntilLoadingHasFinished(); + + // create and save a discover session with filters in ES|QL mode + await discover.clickNewSearchButton(); + await discover.selectTextBaseLang(); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + await monacoEditor.setCodeEditorValue( + 'from logstash-* | sort @timestamp desc | stats averageB = avg(bytes) by extension | sort averageB desc' + ); + await testSubjects.click('querySubmitButton'); + await header.waitUntilLoadingHasFinished(); + await discover.waitUntilSearchingHasFinished(); + expect(await dataGrid.getDocCount()).to.be(5); + await discover.saveSearch(SAVED_DISCOVER_SESSION_WITH_ESQL); + await header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + [SAVED_DISCOVER_SESSION_WITH_DATA_VIEW, SAVED_DISCOVER_SESSION_WITH_ESQL].map( + (title: string) => { + describe(`Generate Embeddable CSV for ${title}`, () => { + beforeEach(async () => { + await kibanaServer.savedObjects.clean({ types: ['dashboard'] }); + }); + + it('generates a report with global time range', async () => { + await dashboard.navigateToApp(); + await dashboard.clickNewDashboard(); + await setDashboardGlobalTimeRange(); + await addEmbeddableToDashboard(title); + + const { text: csvFile } = await getDashboardPanelReport(title); + expectSnapshot(csvFile).toMatch(); + }); + + it('generates a report with custom time range', async () => { + await dashboard.navigateToApp(); + await dashboard.clickNewDashboard(); + await setDashboardGlobalTimeRange(); + await addEmbeddableToDashboard(title); + await setDashboardPanelCustomTimeRange(); + + const { text: csvFile } = await getDashboardPanelReport(title); + expectSnapshot(csvFile).toMatch(); + }); + }); + } + ); + }); +} diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/index.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/index.ts index 859d3a00d1df0..1eff221b095b9 100644 --- a/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/index.ts +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/v1/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('Feature controls', function () { + describe('Saved query management - Feature controls', function () { loadTestFile(require.resolve('./security')); }); } diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts index 916853e1c4591..c9458136ae30f 100644 --- a/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/v2/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('Feature controls - v2', function () { + describe('Saved query management - Feature controls - v2', function () { loadTestFile(require.resolve('./security.v2.ts')); }); } From 9f789e848778e2659ae8c4560cb2d371a5c61e5c Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Tue, 8 Apr 2025 12:24:00 +0200 Subject: [PATCH 3/3] revert --- .../functional/apps/discover/group1/config.ts | 17 - .../functional/apps/discover/group1/index.ts | 15 - .../apps/discover/group1/reporting.ts | 403 ------------------ .../discover/group1/reporting_embeddable.ts | 178 -------- 4 files changed, 613 deletions(-) delete mode 100644 x-pack/test/functional/apps/discover/group1/config.ts delete mode 100644 x-pack/test/functional/apps/discover/group1/index.ts delete mode 100644 x-pack/test/functional/apps/discover/group1/reporting.ts delete mode 100644 x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts diff --git a/x-pack/test/functional/apps/discover/group1/config.ts b/x-pack/test/functional/apps/discover/group1/config.ts deleted file mode 100644 index d0d07ff200281..0000000000000 --- a/x-pack/test/functional/apps/discover/group1/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); - - return { - ...functionalConfig.getAll(), - testFiles: [require.resolve('.')], - }; -} diff --git a/x-pack/test/functional/apps/discover/group1/index.ts b/x-pack/test/functional/apps/discover/group1/index.ts deleted file mode 100644 index d0319d028422d..0000000000000 --- a/x-pack/test/functional/apps/discover/group1/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('discover - group 1', function () { - loadTestFile(require.resolve('./reporting')); // 6 min - loadTestFile(require.resolve('./reporting_embeddable')); // 8 min - }); -} diff --git a/x-pack/test/functional/apps/discover/group1/reporting.ts b/x-pack/test/functional/apps/discover/group1/reporting.ts deleted file mode 100644 index bb71966f5da64..0000000000000 --- a/x-pack/test/functional/apps/discover/group1/reporting.ts +++ /dev/null @@ -1,403 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import moment, { DurationInputArg2 } from 'moment'; -import { Key } from 'selenium-webdriver'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const reportingAPI = getService('reporting'); - const log = getService('log'); - const es = getService('es'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const browser = getService('browser'); - const retry = getService('retry'); - const { reporting, common, discover, timePicker, share, header } = getPageObjects([ - 'reporting', - 'common', - 'discover', - 'timePicker', - 'share', - 'header', - ]); - const monacoEditor = getService('monacoEditor'); - const filterBar = getService('filterBar'); - const testSubjects = getService('testSubjects'); - const toasts = getService('toasts'); - - const deleteIndex = async (index: string) => { - try { - await es.indices.delete({ index }); - } catch (err) { - // ignore 404 error - } - }; - - const createDocs = async ({ - index, - endDate, - docCount, - dateSubstractUnit, - addNumberField, - }: { - index: string; - endDate: string; - docCount: number; - dateSubstractUnit?: DurationInputArg2; - addNumberField?: boolean; - }) => { - interface TestDoc { - timestamp: string; - name: string; - updated_at?: string; - numberValue?: number; - } - - const docs = Array(docCount); - - for (let i = 0; i <= docs.length - 1; i++) { - const name = `test-${i + 1}`; - const timestamp = moment - .utc(endDate) - .subtract(docCount - i, dateSubstractUnit ?? 'days') - .format(); - - const commonFields: Pick = { - timestamp, - name, - }; - - if (addNumberField) { - commonFields.numberValue = i; - } - - if (i === 0) { - // only the oldest document has a value for updated_at - docs[i] = { - ...commonFields, - updated_at: moment.utc(endDate).format(), - }; - } else { - // updated_at field does not exist in first 500 documents - docs[i] = commonFields; - } - } - - const res = await es.bulk({ - index, - operations: docs.map((d) => `{"index": {}}\n${JSON.stringify(d)}\n`), - }); - - log.info(`Indexed ${res.items.length} test data docs into ${index}.`); - }; - - const getReport = async ({ timeout } = { timeout: 60 * 1000 }) => { - // close any open notification toasts - await toasts.dismissAll(); - - await reporting.openExportTab(); - await reporting.clickGenerateReportButton(); - - const url = await reporting.getReportURL(timeout); - const res = await reporting.getResponse(url ?? ''); - - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - return res; - }; - - const getReportPostUrl = async () => { - // click 'Copy POST URL' - await share.clickShareTopNavButton(); - await reporting.openExportTab(); - const copyButton = await testSubjects.find('shareReportingCopyURL'); - - return decodeURIComponent((await copyButton.getAttribute('data-share-url')) ?? ''); - }; - - describe('Discover CSV Export', () => { - describe('Check Available', () => { - before(async () => { - await esArchiver.emptyKibanaIndex(); - await reportingAPI.initEcommerce(); - await common.navigateToApp('discover'); - await discover.selectIndexPattern('ecommerce'); - }); - - after(async () => { - await reportingAPI.teardownEcommerce(); - await esArchiver.emptyKibanaIndex(); - }); - - it('is available if new', async () => { - await reporting.openExportTab(); - expect(await reporting.isGenerateReportButtonDisabled()).to.be(null); - await share.closeShareModal(); - }); - - it('becomes available when saved', async () => { - await discover.saveSearch('my search - expectEnabledGenerateReportButton'); - await reporting.openExportTab(); - expect(await reporting.isGenerateReportButtonDisabled()).to.be(null); - await share.closeShareModal(); - }); - }); - - describe('Generate CSV: new search', () => { - before(async () => { - await reportingAPI.initEcommerce(); - /** - * Important: `esArchiver.emptyKibanaIndex()` above also resets the - * Kibana time zone setting, so we're re-applying it here. - * The serverless version of the test uses - * `kibanaServer.savedObjects.cleanStandardList` instead, - * which does not reset the time zone setting, - * so we don't need to re-apply it in these tests. - */ - await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'UTC' }); - }); - - after(async () => { - await reportingAPI.teardownEcommerce(); - await esArchiver.emptyKibanaIndex(); - }); - - beforeEach(async () => { - await common.navigateToApp('discover'); - await discover.selectIndexPattern('ecommerce'); - }); - - it('generates a report with single timefilter', async () => { - await discover.clickNewSearchButton(); - await timePicker.setCommonlyUsedTime('Last_24 hours'); - await discover.saveSearch('single-timefilter-search'); - - // get shared URL value - const sharedURL = await browser.getCurrentUrl(); - - // click 'Copy POST URL' - await share.clickShareTopNavButton(); - await reporting.openExportTab(); - const copyButton = await testSubjects.find('shareReportingCopyURL'); - const reportURL = decodeURIComponent( - (await copyButton.getAttribute('data-share-url')) ?? '' - ); - - // get number of filters in URLs - const timeFiltersNumberInReportURL = - reportURL.split('query:(range:(order_date:(format:strict_date_optional_time').length - 1; - const timeFiltersNumberInSharedURL = sharedURL.split('time:').length - 1; - - expect(timeFiltersNumberInSharedURL).to.be(1); - expect(sharedURL.includes('time:(from:now-24h%2Fh,to:now))')).to.be(true); - - expect(timeFiltersNumberInReportURL).to.be(1); - - expect( - reportURL.includes( - `query:(range:(order_date:(format:strict_date_optional_time,gte:now-24h/h,lte:now))))` - ) - ).to.be(true); - - // return keyboard state - await browser.getActions().keyUp(Key.CONTROL).perform(); - await browser.getActions().keyUp('v').perform(); - }); - - it('generates a report from a new search with data: default', async () => { - await discover.clickNewSearchButton(); - await reporting.setTimepickerInEcommerceDataRange(); - - await discover.saveSearch('my search - with data - expectReportCanBeCreated'); - - const res = await getReport(); - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - - const csvFile = res.text; - expectSnapshot(csvFile).toMatch(); - }); - - it('generates a report with no data', async () => { - await reporting.setTimepickerInEcommerceNoDataRange(); - await discover.saveSearch('my search - no data - expectReportCanBeCreated'); - - const res = await getReport(); - expect(res.text).to.be(`\n`); - }); - - it('generates a large export', async () => { - const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; - const toTime = 'Aug 23, 2019 @ 16:18:51.821'; - await timePicker.setAbsoluteRange(fromTime, toTime); - await discover.clickNewSearchButton(); - await retry.try(async () => { - expect(await discover.getHitCount()).to.equal('4,675'); - }); - await discover.saveSearch('large export'); - - // match file length, the beginning and the end of the csv file contents - const { text: csvFile } = await getReport({ timeout: 80 * 1000 }); - expect(csvFile.length).to.be(4845684); - expectSnapshot(csvFile.slice(0, 5000)).toMatch(); - expectSnapshot(csvFile.slice(-5000)).toMatch(); - }); - - it('generate a report using ES|QL', async () => { - await discover.selectTextBaseLang(); - const testQuery = `from ecommerce | STATS total_sales = SUM(taxful_total_price) BY day_of_week | SORT total_sales DESC`; - - await monacoEditor.setCodeEditorValue(testQuery); - await testSubjects.click('querySubmitButton'); - await header.waitUntilLoadingHasFinished(); - - const res = await getReport(); - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - - const csvFile = res.text; - expectSnapshot(csvFile).toMatch(); - }); - - it('generate a report using ES|QL for relative time range as absolute dates and time params', async () => { - const RECENT_DATA_INDEX_NAME = 'test_recent_data'; - const RECENT_DOC_COUNT = 500; - const RECENT_DOC_END_DATE = moment().toISOString(); - - await deleteIndex(RECENT_DATA_INDEX_NAME); - await createDocs({ - index: RECENT_DATA_INDEX_NAME, - endDate: RECENT_DOC_END_DATE, - docCount: RECENT_DOC_COUNT, - dateSubstractUnit: 'minutes', - addNumberField: true, - }); - - await timePicker.setCommonlyUsedTime('Last_15 minutes'); - await discover.selectTextBaseLang(); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - - const testQuery = `from ${RECENT_DATA_INDEX_NAME} | sort timestamp | WHERE timestamp >= ?_tstart AND timestamp <= ?_tend | KEEP name, numberValue`; - await monacoEditor.setCodeEditorValue(testQuery); - await testSubjects.click('querySubmitButton'); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - - const reportPostUrl = await getReportPostUrl(); - expect(reportPostUrl).to.contain(`timeRange:(from:'2`); // not `from:now-15m` - expect(reportPostUrl).to.contain(`filters:!()`); - expect(reportPostUrl).to.contain(`query:(esql:'${testQuery}')`); - - const res = await getReport(); - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - - const csvFile = res.text; - expectSnapshot(csvFile).toMatch(); - - await deleteIndex(RECENT_DATA_INDEX_NAME); - }); - }); - - describe('Generate CSV: sparse data', () => { - const TEST_INDEX_NAME = 'sparse_data'; - const TEST_DOC_COUNT = 510; - const TEST_DOC_END_DATE = '2006-08-14T00:00:00'; - - before(async () => { - await deleteIndex(TEST_INDEX_NAME); - await createDocs({ - index: TEST_INDEX_NAME, - endDate: TEST_DOC_END_DATE, - docCount: TEST_DOC_COUNT, - dateSubstractUnit: 'days', - }); - await reportingAPI.initLogs(); - await common.navigateToApp('discover'); - await discover.loadSavedSearch('Sparse Columns'); - }); - - after(async () => { - await reportingAPI.teardownLogs(); - await deleteIndex(TEST_INDEX_NAME); - }); - - beforeEach(async () => { - const fromTime = 'Jan 10, 2005 @ 00:00:00.000'; - const toTime = 'Dec 23, 2006 @ 00:00:00.000'; - await timePicker.setAbsoluteRange(fromTime, toTime); - await retry.try(async () => { - expect(await discover.getHitCount()).to.equal(TEST_DOC_COUNT.toString()); - }); - }); - - it(`handles field formatting for a field that doesn't exist initially`, async () => { - const res = await getReport(); - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - - const csvFile = res.text; - expectSnapshot(csvFile).toMatch(); - }); - }); - - describe('Generate CSV: archived search', () => { - const setupPage = async () => { - const fromTime = 'Jun 22, 2019 @ 00:00:00.000'; - const toTime = 'Jun 26, 2019 @ 23:30:00.000'; - await timePicker.setAbsoluteRange(fromTime, toTime); - }; - - before(async () => { - await reportingAPI.initEcommerce(); - await common.navigateToApp('discover'); - await discover.selectIndexPattern('ecommerce'); - }); - - after(async () => { - await reportingAPI.teardownEcommerce(); - }); - - beforeEach(async () => { - await setupPage(); - }); - - afterEach(async () => { - await reporting.checkForReportingToasts(); - }); - - it('generates a report with data', async () => { - await discover.loadSavedSearch('Ecommerce Data'); - await retry.try(async () => { - expect(await discover.getHitCount()).to.equal('740'); - }); - - const { text: csvFile } = await getReport(); - expectSnapshot(csvFile).toMatch(); - }); - - it('generates a report with filtered data', async () => { - await discover.loadSavedSearch('Ecommerce Data'); - await retry.try(async () => { - expect(await discover.getHitCount()).to.equal('740'); - }); - - // filter - await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` }); - await retry.try(async () => { - expect(await discover.getHitCount()).to.equal('154'); - }); - - const { text: csvFile } = await getReport(); - expectSnapshot(csvFile).toMatch(); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts b/x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts deleted file mode 100644 index 0416ba483d4b3..0000000000000 --- a/x-pack/test/functional/apps/discover/group1/reporting_embeddable.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const retry = getService('retry'); - const { reporting, common, discover, timePicker, header, dashboard, unifiedFieldList } = - getPageObjects([ - 'reporting', - 'common', - 'discover', - 'timePicker', - 'header', - 'dashboard', - 'unifiedFieldList', - ]); - const monacoEditor = getService('monacoEditor'); - const queryBar = getService('queryBar'); - const testSubjects = getService('testSubjects'); - const toasts = getService('toasts'); - const dataGrid = getService('dataGrid'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const dashboardPanelActions = getService('dashboardPanelActions'); - const dashboardCustomizePanel = getService('dashboardCustomizePanel'); - const dashboardBadgeActions = getService('dashboardBadgeActions'); - - const GENERATE_CSV_DATA_TEST_SUBJ = 'embeddablePanelAction-generateCsvReport'; - const SAVED_DISCOVER_SESSION_WITH_DATA_VIEW = 'savedDiscoverSessionWithDataView'; - const SAVED_DISCOVER_SESSION_WITH_ESQL = 'savedDiscoverSessionWithESQL'; - - const getDashboardPanelReport = async (title: string, { timeout } = { timeout: 60 * 1000 }) => { - await toasts.dismissAll(); - - await dashboardPanelActions.expectExistsPanelAction(GENERATE_CSV_DATA_TEST_SUBJ, title); - await dashboardPanelActions.clickPanelActionByTitle(GENERATE_CSV_DATA_TEST_SUBJ, title); - await testSubjects.existOrFail('csvReportStarted'); /* validate toast panel */ - - const url = await reporting.getReportURL(timeout); - const res = await reporting.getResponse(url ?? ''); - - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - return res; - }; - - const addEmbeddableToDashboard = async (title: string) => { - await dashboardAddPanel.addSavedSearch(title); - await header.waitUntilLoadingHasFinished(); - await dashboard.waitForRenderComplete(); - const rows = await dataGrid.getDocTableRows(); - expect(rows.length).to.be.above(0); - await dashboard.saveDashboard('test-csv', { - saveAsNew: true, - waitDialogIsClosed: false, - exitFromEditMode: true, - }); - await header.waitUntilLoadingHasFinished(); - await dashboard.waitForRenderComplete(); - }; - - const setDashboardGlobalTimeRange = async () => { - const fromTime = 'Sep 19, 2015 @ 16:31:44.000'; - const toTime = 'Sep 21, 2015 @ 11:31:44.000'; - await timePicker.setAbsoluteRange(fromTime, toTime); - }; - - const setDashboardPanelCustomTimeRange = async () => { - await dashboardPanelActions.customizePanel(); - await dashboardCustomizePanel.enableCustomTimeRange(); - - const toTime = 'Sep 21, 2015 @ 01:31:44.000'; - const endTimeTestSubj = - 'customizePanelTimeRangeDatePicker > superDatePickerendDatePopoverButton'; - await retry.waitFor(`endDate is set to ${toTime}`, async () => { - await testSubjects.click(endTimeTestSubj); - await testSubjects.click('superDatePickerAbsoluteDateInput'); - await timePicker.inputValue('superDatePickerAbsoluteDateInput', toTime); - await testSubjects.click(endTimeTestSubj); - const actualToTime = await testSubjects.getVisibleText(endTimeTestSubj); - return toTime === actualToTime; - }); - - await dashboardCustomizePanel.clickSaveButton(); - await dashboard.waitForRenderComplete(); - await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); - }; - - describe('Discover Embeddable - Generate CSV report per panel', () => { - before(async () => { - await kibanaServer.savedObjects.cleanStandardList(); - await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); - await kibanaServer.importExport.load( - 'src/platform/test/functional/fixtures/kbn_archiver/discover' - ); - await timePicker.setDefaultAbsoluteRangeViaUiSettings(); - await common.navigateToApp('discover'); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - await discover.selectIndexPattern('logstash-*'); - - // create and save a discover session with filters in data view mode - await unifiedFieldList.waitUntilSidebarHasLoaded(); - await unifiedFieldList.clickFieldListItem('geo.src'); - await unifiedFieldList.clickFieldListPlusFilter('geo.src', 'US'); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - await unifiedFieldList.clickFieldListItemAdd('extension'); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - await queryBar.setQuery('machine.os : ios'); - await queryBar.submitQuery(); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - expect(await dataGrid.getDocCount()).to.be(222); - await discover.saveSearch(SAVED_DISCOVER_SESSION_WITH_DATA_VIEW); - await header.waitUntilLoadingHasFinished(); - - // create and save a discover session with filters in ES|QL mode - await discover.clickNewSearchButton(); - await discover.selectTextBaseLang(); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - await monacoEditor.setCodeEditorValue( - 'from logstash-* | sort @timestamp desc | stats averageB = avg(bytes) by extension | sort averageB desc' - ); - await testSubjects.click('querySubmitButton'); - await header.waitUntilLoadingHasFinished(); - await discover.waitUntilSearchingHasFinished(); - expect(await dataGrid.getDocCount()).to.be(5); - await discover.saveSearch(SAVED_DISCOVER_SESSION_WITH_ESQL); - await header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); - await kibanaServer.savedObjects.cleanStandardList(); - }); - - [SAVED_DISCOVER_SESSION_WITH_DATA_VIEW, SAVED_DISCOVER_SESSION_WITH_ESQL].map( - (title: string) => { - describe(`Generate Embeddable CSV for ${title}`, () => { - beforeEach(async () => { - await kibanaServer.savedObjects.clean({ types: ['dashboard'] }); - }); - - it('generates a report with global time range', async () => { - await dashboard.navigateToApp(); - await dashboard.clickNewDashboard(); - await setDashboardGlobalTimeRange(); - await addEmbeddableToDashboard(title); - - const { text: csvFile } = await getDashboardPanelReport(title); - expectSnapshot(csvFile).toMatch(); - }); - - it('generates a report with custom time range', async () => { - await dashboard.navigateToApp(); - await dashboard.clickNewDashboard(); - await setDashboardGlobalTimeRange(); - await addEmbeddableToDashboard(title); - await setDashboardPanelCustomTimeRange(); - - const { text: csvFile } = await getDashboardPanelReport(title); - expectSnapshot(csvFile).toMatch(); - }); - }); - } - ); - }); -}