diff --git a/.gitignore b/.gitignore index 0d9529eb65e54..45034583cffbb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,21 @@ target .idea *.iml *.log + +# Ignore certain functional test runner artifacts /test/*/failure_debug /test/*/screenshots/diff /test/*/screenshots/failure /test/*/screenshots/session /test/*/screenshots/visual_regression_gallery.html + +# Ignore the same artifacts in x-pack +/x-pack/test/*/failure_debug +/x-pack/test/*/screenshots/diff +/x-pack/test/*/screenshots/failure +/x-pack/test/*/screenshots/session +/x-pack/test/*/screenshots/visual_regression_gallery.html + /html_docs .eslintcache /plugins/ diff --git a/test/functional/services/common/screenshots.ts b/test/functional/services/common/screenshots.ts index 5bce0d4cf6c87..6e492ad1ced19 100644 --- a/test/functional/services/common/screenshots.ts +++ b/test/functional/services/common/screenshots.ts @@ -61,6 +61,8 @@ export async function ScreenshotsProvider({ getService }: FtrProviderContext) { if (updateBaselines) { log.debug('Updating baseline snapshot'); + // Make the directory if it doesn't exist + await mkdirAsync(dirname(baselinePath), { recursive: true }); await writeFileAsync(baselinePath, readFileSync(sessionPath)); return 0; } else { diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 50cc7eaa378ea..e4b8a7f477abb 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -215,7 +215,8 @@ export function mockTreeWithNoAncestorsAnd2Children({ const secondChild: SafeResolverEvent = mockEndpointEvent({ pid: 2, entityID: secondChildID, - processName: 'e', + processName: + 'really_really_really_really_really_really_really_really_really_really_really_really_really_really_long_node_name', parentEntityID: originID, timestamp: 1600863932318, }); @@ -388,5 +389,31 @@ export function mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ eventCategory: 'registry', }), ]; + // Add one additional event for each category + const categories: string[] = [ + 'authentication', + 'database', + 'driver', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'web', + ]; + for (const category of categories) { + relatedEvents.push( + mockEndpointEvent({ + entityID: originID, + parentEntityID, + eventID: `${relatedEvents.length}`, + eventType: 'access', + eventCategory: category, + }) + ); + } return withRelatedEventsOnOrigin(baseTree, relatedEvents); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 7739d81269180..198f0dc7905e9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -208,7 +208,7 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', }); }); -describe('Resolver, when analyzing a tree that has two related events for the origin', () => { +describe('Resolver, when analyzing a tree that has 2 related registry and 1 related event of all other categories for the origin node', () => { beforeEach(async () => { // create a mock data access layer with related events const { @@ -282,7 +282,21 @@ describe('Resolver, when analyzing a tree that has two related events for the or simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text()) ) - ).toYieldEqualTo(['2 registry']); + ).toYieldEqualTo([ + '1 authentication', + '1 database', + '1 driver', + '1 file', + '1 host', + '1 iam', + '1 intrusion_detection', + '1 malware', + '1 network', + '1 package', + '1 process', + '2 registry', + '1 web', + ]); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 3b3651ec2558a..34a6d5fffc7ee 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -13,7 +13,7 @@ import { urlSearch } from '../test_utilities/url_search'; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; -describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => { +describe(`Resolver: when analyzing a tree with no ancestors and two children and 2 related registry events and 1 event of each other category on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => { /** * Get (or lazily create and get) the simulator. */ @@ -272,22 +272,33 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and await expect( simulator().map(() => { // The link text is split across two columns. The first column is the count and the second column has the type. + const typesAndCounts: Array<{ type: string; link: string }> = []; const type = simulator().testSubject('resolver:panel:node-events:event-type-count'); const link = simulator().testSubject('resolver:panel:node-events:event-type-link'); - return { - typeLength: type.length, - linkLength: link.length, - typeText: type.text(), - linkText: link.text(), - }; + for (let index = 0; index < type.length; index++) { + typesAndCounts.push({ + type: type.at(index).text(), + link: link.at(index).text(), + }); + } + return typesAndCounts; }) - ).toYieldEqualTo({ - typeLength: 1, - linkLength: 1, - linkText: 'registry', - // EUI's Table adds the column name to the value. - typeText: 'Count2', - }); + ).toYieldEqualTo([ + // Because there is no printed whitespace after "Count", the count immediately follows it. + { link: 'registry', type: 'Count2' }, + { link: 'authentication', type: 'Count1' }, + { link: 'database', type: 'Count1' }, + { link: 'driver', type: 'Count1' }, + { link: 'file', type: 'Count1' }, + { link: 'host', type: 'Count1' }, + { link: 'iam', type: 'Count1' }, + { link: 'intrusion_detection', type: 'Count1' }, + { link: 'malware', type: 'Count1' }, + { link: 'network', type: 'Count1' }, + { link: 'package', type: 'Count1' }, + { link: 'process', type: 'Count1' }, + { link: 'web', type: 'Count1' }, + ]); }); describe('and when the user clicks the registry events link', () => { beforeEach(async () => { @@ -377,7 +388,11 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and .testSubject('resolver:node-list:node-link:title') .map((node) => node.text()); }) - ).toYieldEqualTo(['c.ext', 'd', 'e']); + ).toYieldEqualTo([ + 'c.ext', + 'd', + 'really_really_really_really_really_really_really_really_really_really_really_really_really_really_long_node_name', + ]); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index b5324b82faa71..6312991ddb743 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -12,41 +12,15 @@ import { ResolverNodeStats } from '../../../common/endpoint/types'; import { useRelatedEventByCategoryNavigation } from './use_related_event_by_category_navigation'; import { useColors } from './use_colors'; -/** - * i18n-translated titles for submenus and identifiers for display of states: - * initialMenuStatus: submenu before it has been opened / requested data - * menuError: if the submenu requested data, but received an error - */ -export const subMenuAssets = { - initialMenuStatus: i18n.translate( - 'xpack.securitySolution.endpoint.resolver.relatedNotRetrieved', - { - defaultMessage: 'Related Events have not yet been retrieved.', - } - ), - menuError: i18n.translate('xpack.securitySolution.endpoint.resolver.relatedRetrievalError', { - defaultMessage: 'There was an error retrieving related events.', - }), - relatedEvents: { - title: i18n.translate('xpack.securitySolution.endpoint.resolver.relatedEvents', { - defaultMessage: 'Events', - }), - }, -}; - -interface ResolverSubmenuOption { - optionTitle: string; - action: () => unknown; - prefix?: number | JSX.Element; -} - /** * Until browser support accomodates the `notation="compact"` feature of Intl.NumberFormat... * exported for testing * @param num The number to format * @returns [mantissa ("12" in "12k+"), Scalar of compact notation (k,M,B,T), remainder indicator ("+" in "12k+")] */ -export function compactNotationParts(num: number): [number, string, string] { +export function compactNotationParts( + num: number +): [mantissa: number, compactNotation: string, remainderIndicator: string] { if (!Number.isFinite(num)) { return [num, '', '']; } @@ -85,8 +59,6 @@ export function compactNotationParts(num: number): [number, string, string] { return [Math.floor(num / scale), prefix, (num / scale) % 1 > Number.EPSILON ? hasRemainder : '']; } -export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string; - /** * A Submenu that displays a collection of "pills" for each related event * category it has events for. diff --git a/x-pack/plugins/security_solution/server/lib/hosts/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/hosts/helpers.test.ts index ae2451e693ce3..99d5ab85cc547 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/helpers.test.ts @@ -13,6 +13,7 @@ describe('#buildFieldsTermAggregation', () => { const fields: readonly string[] = [ 'host.architecture', 'host.id', + 'host.ip', 'host.name', 'host.os.family', 'host.os.name', @@ -50,6 +51,25 @@ describe('#buildFieldsTermAggregation', () => { }, }, }, + host_ip: { + terms: { + script: { + source: "doc['host.ip']", + lang: 'painless', + }, + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, + }, host_name: { terms: { field: 'host.name', diff --git a/x-pack/plugins/security_solution/server/lib/hosts/helpers.ts b/x-pack/plugins/security_solution/server/lib/hosts/helpers.ts index 886c2d521caba..350bbac8654f0 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/helpers.ts @@ -16,6 +16,30 @@ export const buildFieldsTermAggregation = (esFields: readonly string[]): Aggrega ); const getTermsAggregationTypeFromField = (field: string): AggregationRequest => { + if (field === 'host.ip') { + return { + host_ip: { + terms: { + script: { + source: "doc['host.ip']", + lang: 'painless', + }, + size: 10, + order: { + timestamp: 'desc', + }, + }, + aggs: { + timestamp: { + max: { + field: '@timestamp', + }, + }, + }, + }, + }; + } + return { [field.replace(/\./g, '_')]: { terms: { diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index c735412aedbf5..67967f2a3cc7e 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -143,8 +143,13 @@ export interface MSearchHeader { export interface AggregationRequest { [aggField: string]: { terms?: { - field: string; + field?: string; + missing?: string; size?: number; + script?: { + source: string; + lang: string; + }; order?: { [aggSortField: string]: SortRequestDirection; }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts index dbd2591c8af7b..8f61ef18a0855 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/__mocks__/index.ts @@ -1296,7 +1296,101 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { export const formattedSearchStrategyResponse = { inspect: { dsl: [ - '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "host_architecture": {\n "terms": {\n "field": "host.architecture",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_id": {\n "terms": {\n "field": "host.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_ip": {\n "terms": {\n "field": "host.ip",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_mac": {\n "terms": {\n "field": "host.mac",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_name": {\n "terms": {\n "field": "host.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_family": {\n "terms": {\n "field": "host.os.family",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_name": {\n "terms": {\n "field": "host.os.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_platform": {\n "terms": {\n "field": "host.os.platform",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_version": {\n "terms": {\n "field": "host.os.version",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_instance_id": {\n "terms": {\n "field": "cloud.instance.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_machine_type": {\n "terms": {\n "field": "cloud.machine.type",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_provider": {\n "terms": {\n "field": "cloud.provider",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_region": {\n "terms": {\n "field": "cloud.region",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n {\n "term": {\n "host.name": "bastion00.siem.estc.dev"\n }\n },\n {\n "range": {\n "@timestamp": {\n "format": "strict_date_optional_time",\n "gte": "2020-09-02T15:17:13.678Z",\n "lte": "2020-09-03T15:17:13.678Z"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', + JSON.stringify( + { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggregations: { + host_architecture: { + terms: { field: 'host.architecture', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_id: { + terms: { field: 'host.id', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_ip: { + terms: { + script: { source: "doc['host.ip']", lang: 'painless' }, + size: 10, + order: { timestamp: 'desc' }, + }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_mac: { + terms: { field: 'host.mac', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_name: { + terms: { field: 'host.name', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_family: { + terms: { field: 'host.os.family', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_name: { + terms: { field: 'host.os.name', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_platform: { + terms: { field: 'host.os.platform', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_version: { + terms: { field: 'host.os.version', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_instance_id: { + terms: { field: 'cloud.instance.id', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_machine_type: { + terms: { field: 'cloud.machine.type', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_provider: { + terms: { field: 'cloud.provider', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_region: { + terms: { field: 'cloud.region', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + }, + query: { + bool: { + filter: [ + { term: { 'host.name': 'bastion00.siem.estc.dev' } }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-09-02T15:17:13.678Z', + lte: '2020-09-03T15:17:13.678Z', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: false, + }, + }, + null, + 2 + ), ], }, hostDetails: {}, @@ -1350,7 +1444,10 @@ export const expectedDsl = { }, host_ip: { terms: { - field: 'host.ip', + script: { + source: "doc['host.ip']", + lang: 'painless', + }, size: 10, order: { timestamp: 'desc', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index aa429e416e715..4eee3fbc37c92 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17660,10 +17660,7 @@ "xpack.securitySolution.endpoint.resolver.processDescription": "{isEventBeingAnalyzed, select, true {分析されたイベント· {descriptionText}} false {{descriptionText}}}", "xpack.securitySolution.endpoint.resolver.relatedEventLimitExceeded": "{numberOfEventsMissing} {category}件のイベントを表示できませんでした。データの上限に達しました。", "xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "このリストには、{numberOfEntries}件のプロセスイベントが含まれています。", - "xpack.securitySolution.endpoint.resolver.relatedEvents": "イベント", "xpack.securitySolution.endpoint.resolver.relatedLimitsExceededTitle": "このリストには、{numberOfEventsDisplayed} {category}件のイベントが含まれます。", - "xpack.securitySolution.endpoint.resolver.relatedNotRetrieved": "関連するイベントがまだ取得されていません。", - "xpack.securitySolution.endpoint.resolver.relatedRetrievalError": "関連するイベントの取得中にエラーが発生しました。", "xpack.securitySolution.endpoint.resolver.runningProcess": "プロセスの実行中", "xpack.securitySolution.endpoint.resolver.runningTrigger": "トリガーの実行中", "xpack.securitySolution.endpoint.resolver.terminatedProcess": "プロセスを中断しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e8957a96e460f..c1645fc381494 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17679,10 +17679,7 @@ "xpack.securitySolution.endpoint.resolver.processDescription": "{isEventBeingAnalyzed, select, true {已分析的事件 · {descriptionText}} false {{descriptionText}}}", "xpack.securitySolution.endpoint.resolver.relatedEventLimitExceeded": "{numberOfEventsMissing} 个{category}事件无法显示,因为已达到数据限制。", "xpack.securitySolution.endpoint.resolver.relatedEventLimitTitle": "此列表包括 {numberOfEntries} 个进程事件。", - "xpack.securitySolution.endpoint.resolver.relatedEvents": "事件", "xpack.securitySolution.endpoint.resolver.relatedLimitsExceededTitle": "此列表包括 {numberOfEventsDisplayed} 个{category}事件。", - "xpack.securitySolution.endpoint.resolver.relatedNotRetrieved": "尚未检索相关事件。", - "xpack.securitySolution.endpoint.resolver.relatedRetrievalError": "检索相关事件时出现错误。", "xpack.securitySolution.endpoint.resolver.runningProcess": "正在运行的进程", "xpack.securitySolution.endpoint.resolver.runningTrigger": "正在运行的触发器", "xpack.securitySolution.endpoint.resolver.terminatedProcess": "已终止进程", diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 37d35662eb15b..cb0b9f63906ce 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -5,7 +5,6 @@ */ import { resolve } from 'path'; import fs from 'fs'; -import { KIBANA_ROOT } from '@kbn/test'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; import { pageObjects } from './page_objects'; @@ -14,9 +13,7 @@ import { pageObjects } from './page_objects'; // that returns an object with the projects config values export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile( - require.resolve('../security_solution_endpoint/config.ts') - ); + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); @@ -43,12 +40,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), ...plugins.map((pluginDir) => `--plugin-path=${resolve(__dirname, 'plugins', pluginDir)}`), - `--plugin-path=${resolve( - KIBANA_ROOT, - 'test/plugin_functional/plugins/core_provider_plugin' - )}`, - // Required to load new platform plugins via `--plugin-path` flag. - '--env.name=development', ], }, uiSettings: xpackFunctionalConfig.get('uiSettings'), diff --git a/x-pack/test/plugin_functional/screenshots/baseline/first_child.png b/x-pack/test/plugin_functional/screenshots/baseline/first_child.png new file mode 100644 index 0000000000000..2d9fd1f039119 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/first_child.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/first_child_selected.png b/x-pack/test/plugin_functional/screenshots/baseline/first_child_selected.png new file mode 100644 index 0000000000000..b5b7644e3ccf5 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/first_child_selected.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/first_child_selected_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/first_child_selected_with_primary_button_hovered.png new file mode 100644 index 0000000000000..b73aa39c03e7a Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/first_child_selected_with_primary_button_hovered.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/first_child_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/first_child_with_primary_button_hovered.png new file mode 100644 index 0000000000000..9eacdc920bacd Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/first_child_with_primary_button_hovered.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin.png b/x-pack/test/plugin_functional/screenshots/baseline/origin.png new file mode 100644 index 0000000000000..5df290b3a2cff Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/origin.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin_selected.png b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected.png new file mode 100644 index 0000000000000..e1da213120fb5 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_first_pill_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_first_pill_hovered.png new file mode 100644 index 0000000000000..e1da213120fb5 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_first_pill_hovered.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_first_pill_selected.png b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_first_pill_selected.png new file mode 100644 index 0000000000000..85de76fadb324 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_first_pill_selected.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_primary_button_hovered.png new file mode 100644 index 0000000000000..b1fd64d9f0b70 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/origin_selected_with_primary_button_hovered.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/origin_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/origin_with_primary_button_hovered.png new file mode 100644 index 0000000000000..ac3892e492058 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/origin_with_primary_button_hovered.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/second_child.png b/x-pack/test/plugin_functional/screenshots/baseline/second_child.png new file mode 100644 index 0000000000000..41b34be569ce9 Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/second_child.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected.png b/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected.png new file mode 100644 index 0000000000000..59277eb63f85f Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected_with_primary_button_hovered.png new file mode 100644 index 0000000000000..5eb5828f5a74d Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/second_child_selected_with_primary_button_hovered.png differ diff --git a/x-pack/test/plugin_functional/screenshots/baseline/second_child_with_primary_button_hovered.png b/x-pack/test/plugin_functional/screenshots/baseline/second_child_with_primary_button_hovered.png new file mode 100644 index 0000000000000..3854dc2c3e0fe Binary files /dev/null and b/x-pack/test/plugin_functional/screenshots/baseline/second_child_with_primary_button_hovered.png differ diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts index 8cdf54a50bc53..c6029fb9ad7ce 100644 --- a/x-pack/test/plugin_functional/test_suites/resolver/index.ts +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -4,24 +4,174 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; + import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getPageObjects, getService }: FtrProviderContext) { +const expectedDifference = 0.09; + +export default function ({ + getPageObjects, + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { const pageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); + const screenshot = getService('screenshots'); + const find = getService('find'); + const browser = getService('browser'); describe('Resolver test app', function () { this.tags('ciGroup7'); - beforeEach(async function () { + // Note: these tests are intended to run on the same page in serial. + before(async function () { await pageObjects.common.navigateToApp('resolverTest'); + // make the window big enough that all nodes are fully in view (for screenshots) + await browser.setScreenshotSize(3840, 2400); }); - it('renders at least one node, one node-list, one edge line, and graph controls', async function () { + it('renders at least one node', async () => { await testSubjects.existOrFail('resolver:node'); + }); + it('renders a node list', async () => { await testSubjects.existOrFail('resolver:node-list'); + }); + it('renders at least one edge line', async () => { await testSubjects.existOrFail('resolver:graph:edgeline'); + }); + it('renders graph controls', async () => { await testSubjects.existOrFail('resolver:graph-controls'); }); + /** + * The mock data used to render the Resolver test plugin has 3 nodes: + * - an origin node with 13 related event pills + * - a non-origin node with a long name + * - a non-origin node with a short name + * + * Each node is captured when selected and unselected. + * + * For each node is captured (once when selected and once when unselected) in each of the following interaction states: + * - primary button hovered + * - pill is hovered + * - pill is clicked + * - pill is clicked and hovered + */ + + // Because the lint rules will not allow files that include upper case characters, we specify explicit file name prefixes + const nodeDefinitions: Array<[nodeID: string, fileNamePrefix: string, hasAPill: boolean]> = [ + ['origin', 'origin', true], + ['firstChild', 'first_child', false], + ['secondChild', 'second_child', false], + ]; + + for (const [nodeID, fileNamePrefix, hasAPill] of nodeDefinitions) { + describe(`when the user is interacting with the node with ID: ${nodeID}`, () => { + let element: () => Promise; + beforeEach(async () => { + element = () => find.byCssSelector(`[data-test-resolver-node-id="${nodeID}"]`); + }); + it('should render as expected', async () => { + expect( + await screenshot.compareAgainstBaseline( + `${fileNamePrefix}`, + updateBaselines, + await element() + ) + ).to.be.lessThan(expectedDifference); + }); + describe('when the user hovers over the primary button', () => { + let button: WebElementWrapper; + beforeEach(async () => { + // hover the button + button = await (await element()).findByCssSelector( + `button[data-test-resolver-node-id="${nodeID}"]` + ); + await button.moveMouseTo(); + }); + it('should render as expected', async () => { + expect( + await screenshot.compareAgainstBaseline( + `${fileNamePrefix}_with_primary_button_hovered`, + updateBaselines, + await element() + ) + ).to.be.lessThan(expectedDifference); + }); + describe('when the user has clicked the primary button (which selects the node.)', () => { + beforeEach(async () => { + // select the node + await button.click(); + }); + it('should render as expected', async () => { + expect( + await screenshot.compareAgainstBaseline( + `${fileNamePrefix}_selected_with_primary_button_hovered`, + updateBaselines, + await element() + ) + ).to.be.lessThan(expectedDifference); + }); + describe('when the user has moved their mouse off of the primary button (and onto the zoom controls.)', () => { + beforeEach(async () => { + // move the mouse away + const zoomIn = await testSubjects.find('resolver:graph-controls:zoom-in'); + await zoomIn.moveMouseTo(); + }); + it('should render as expected', async () => { + expect( + await screenshot.compareAgainstBaseline( + `${fileNamePrefix}_selected`, + updateBaselines, + await element() + ) + ).to.be.lessThan(expectedDifference); + }); + if (hasAPill) { + describe('when the user hovers over the first pill', () => { + let firstPill: () => Promise; + beforeEach(async () => { + firstPill = async () => { + // select a pill + const pills = await (await element()).findAllByTestSubject( + 'resolver:map:node-submenu-item' + ); + return pills[0]; + }; + + // move mouse to first pill + await (await firstPill()).moveMouseTo(); + }); + it('should render as expected', async () => { + const diff = await screenshot.compareAgainstBaseline( + `${fileNamePrefix}_selected_with_first_pill_hovered`, + updateBaselines, + await element() + ); + expect(diff).to.be.lessThan(expectedDifference); + }); + describe('when the user clicks on the first pill', () => { + beforeEach(async () => { + // click the first pill + await (await firstPill()).click(); + }); + it('should render as expected', async () => { + expect( + await screenshot.compareAgainstBaseline( + `${fileNamePrefix}_selected_with_first_pill_selected`, + updateBaselines, + await element() + ) + ).to.be.lessThan(expectedDifference); + }); + }); + }); + } + }); + }); + }); + }); + } }); }