diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 66b4b164a794c..cc985407698bf 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -19,5 +19,6 @@ module.exports = { coverageReporters: ['text', 'html'], collectCoverageFrom: [ '/x-pack/plugins/apm/{common,public,server}/**/*.{js,ts,tsx}', + '!/**/*.stories.*', ], }; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx new file mode 100644 index 0000000000000..0a4adc07e1a98 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx @@ -0,0 +1,81 @@ +/* + * 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 { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; +import { AnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { ServiceInventory } from './'; + +const stories: Meta<{}> = { + title: 'app/ServiceInventory', + component: ServiceInventory, + decorators: [ + (StoryComponent) => { + const coreMock = { + http: { + get: (endpoint: string) => { + switch (endpoint) { + case '/internal/apm/services': + return { items: [] }; + default: + return {}; + } + return {}; + }, + }, + notifications: { toasts: { add: () => {}, addWarning: () => {} } }, + uiSettings: { get: () => [] }, + } as unknown as CoreStart; + + const KibanaReactContext = createKibanaReactContext(coreMock); + + const anomlyDetectionJobsContextValue = { + anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, + anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, + anomalyDetectionJobsRefetch: () => {}, + }; + + return ( + + + + + + + + + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story<{}> = () => { + return ; +}; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index d62955b593df1..36b1053248d25 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -5,249 +5,17 @@ * 2.0. */ -import { render, waitFor } from '@testing-library/react'; -import { CoreStart } from 'kibana/public'; -import { merge } from 'lodash'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ServiceHealthStatus } from '../../../../common/service_health_status'; -import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; -import { ServiceInventory } from '.'; -import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; -import { - mockApmPluginContextValue, - MockApmPluginContextWrapper, -} from '../../../context/apm_plugin/mock_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { clearCache } from '../../../services/rest/callApi'; -import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; -import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock'; -import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; -import * as hook from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; +import { composeStories } from '@storybook/testing-react'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import * as stories from './service_inventory.stories'; -const KibanaReactContext = createKibanaReactContext({ - usageCollection: { reportUiCounter: () => {} }, -} as Partial); - -const addWarning = jest.fn(); -const httpGet = jest.fn(); - -function wrapper({ children }: { children?: ReactNode }) { - const mockPluginContext = merge({}, mockApmPluginContextValue, { - core: { - http: { - get: httpGet, - }, - notifications: { - toasts: { - addWarning, - }, - }, - }, - }) as unknown as ApmPluginContextValue; - - return ( - - - - - - {children} - - - - - - ); -} +const { Example } = composeStories(stories); describe('ServiceInventory', () => { - beforeEach(() => { - // @ts-expect-error - global.sessionStorage = new SessionStorageMock(); - clearCache(); - - jest.spyOn(hook, 'useAnomalyDetectionJobsContext').mockReturnValue({ - anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, - anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, - anomalyDetectionJobsRefetch: () => {}, - }); - - jest - .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPatternFetcher') - .mockReturnValue({ - indexPattern: undefined, - status: FETCH_STATUS.SUCCESS, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should render services, when list is not empty', async () => { - // mock rest requests - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - { - serviceName: 'My Go Service', - agentName: 'go', - transactionsPerMinute: 400, - errorsPerMinute: 500, - avgResponseTime: 600, - environments: [], - severity: ServiceHealthStatus.healthy, - }, - ], - }); - - const { container, findByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - await findByText('My Python Service'); - - expect(container.querySelectorAll('.euiTableRow')).toHaveLength(2); - }); - - it('should render empty message, when list is empty and historical data is found', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); - - const { findByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - const noServicesText = await findByText('No services found'); - - expect(noServicesText).not.toBeEmptyDOMElement(); - }); - - describe('when legacy data is found', () => { - it('renders an upgrade migration notification', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: true, - hasHistoricalData: true, - items: [], - }); - - render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(addWarning).toHaveBeenLastCalledWith( - expect.objectContaining({ - title: 'Legacy data was detected within the selected time range', - }) - ); - }); - }); - - describe('when legacy data is not found', () => { - it('does not render an upgrade migration notification', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); - - render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(addWarning).not.toHaveBeenCalled(); - }); - }); - - describe('when ML data is not found', () => { - it('does not render the health column', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - }, - ], - }); - - const { queryByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(queryByText('Health')).toBeNull(); - }); - }); - - describe('when ML data is found', () => { - it('renders the health column', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - ], - }); - - const { queryAllByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); + it('renders', async () => { + render(); - expect(queryAllByText('Health').length).toBeGreaterThan(1); - }); + expect(await screen.findByRole('table')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx new file mode 100644 index 0000000000000..b632d3a33dea8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx @@ -0,0 +1,76 @@ +/* + * 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 { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import type { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import type { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { + APMServiceContext, + APMServiceContextValue, +} from '../../../context/apm_service/apm_service_context'; +import { ServiceOverview } from './'; + +const stories: Meta<{}> = { + title: 'app/ServiceOverview', + component: ServiceOverview, + decorators: [ + (StoryComponent) => { + const serviceName = 'testServiceName'; + const mockCore = { + http: { + basePath: { prepend: () => {} }, + get: (endpoint: string) => { + switch (endpoint) { + case `/api/apm/services/${serviceName}/annotation/search`: + return { annotations: [] }; + case '/internal/apm/fallback_to_transactions': + return { fallbackToTransactions: false }; + case `/internal/apm/services/${serviceName}/dependencies`: + return { serviceDependencies: [] }; + default: + return {}; + } + }, + }, + notifications: { toasts: { add: () => {} } }, + uiSettings: { get: () => 'Browser' }, + } as unknown as CoreStart; + const serviceContextValue = { + alerts: [], + serviceName, + } as unknown as APMServiceContextValue; + const KibanaReactContext = createKibanaReactContext(mockCore); + + return ( + + + + + + + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story<{}> = () => { + return ; +}; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 42a0c68535efe..fb60604aa53b2 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -5,180 +5,19 @@ * 2.0. */ -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { CoreStart } from 'src/core/public'; -import { isEqual } from 'lodash'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; -import { - mockApmPluginContextValue, - MockApmPluginContextWrapper, -} from '../../../context/apm_plugin/mock_apm_plugin_context'; -import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context'; -import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_breakdown_chart/use_transaction_breakdown'; -import { renderWithTheme } from '../../../utils/testHelpers'; -import { ServiceOverview } from './'; -import { waitFor } from '@testing-library/dom'; -import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { - getCallApmApiSpy, - getCreateCallApmApiSpy, -} from '../../../services/rest/callApmApiSpy'; -import { fromQuery } from '../../shared/Links/url_helpers'; -import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; -import { uiSettingsServiceMock } from '../../../../../../../src/core/public/mocks'; +import { composeStories } from '@storybook/testing-react'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import * as stories from './service_overview.stories'; -const uiSettings = uiSettingsServiceMock.create().setup({} as any); - -const KibanaReactContext = createKibanaReactContext({ - notifications: { toasts: { add: () => {} } }, - uiSettings, - usageCollection: { reportUiCounter: () => {} }, -} as unknown as Partial); - -const mockParams = { - rangeFrom: 'now-15m', - rangeTo: 'now', - latencyAggregationType: LatencyAggregationType.avg, -}; - -const location = { - pathname: '/services/test%20service%20name/overview', - search: fromQuery(mockParams), -}; - -function Wrapper({ children }: { children?: ReactNode }) { - const value = { - ...mockApmPluginContextValue, - core: { - ...mockApmPluginContextValue.core, - http: { - basePath: { prepend: () => {} }, - get: () => {}, - }, - }, - } as unknown as ApmPluginContextValue; - - return ( - - - - - {children} - - - - - ); -} +const { Example } = composeStories(stories); describe('ServiceOverview', () => { it('renders', async () => { - jest - .spyOn(useApmServiceContextHooks, 'useApmServiceContext') - .mockReturnValue({ - serviceName: 'test service name', - agentName: 'java', - transactionType: 'request', - transactionTypes: ['request'], - alerts: [], - }); - jest - .spyOn(useAnnotationsHooks, 'useAnnotationsContext') - .mockReturnValue({ annotations: [] }); - jest - .spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPatternFetcher') - .mockReturnValue({ - indexPattern: undefined, - status: FETCH_STATUS.SUCCESS, - }); - - /* eslint-disable @typescript-eslint/naming-convention */ - const calls = { - 'GET /internal/apm/services/{serviceName}/error_groups/main_statistics': { - error_groups: [] as any[], - }, - 'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics': - { - transactionGroups: [] as any[], - totalTransactionGroups: 0, - isAggregationAccurate: true, - }, - 'GET /internal/apm/services/{serviceName}/dependencies': { - serviceDependencies: [], - }, - 'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics': - [], - 'GET /internal/apm/services/{serviceName}/transactions/charts/latency': { - currentPeriod: { - overallAvgDuration: null, - latencyTimeseries: [], - }, - previousPeriod: { - overallAvgDuration: null, - latencyTimeseries: [], - }, - }, - 'GET /internal/apm/services/{serviceName}/throughput': { - currentPeriod: [], - previousPeriod: [], - }, - 'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate': - { - currentPeriod: { - transactionErrorRate: [], - noHits: true, - average: null, - }, - previousPeriod: { - transactionErrorRate: [], - noHits: true, - average: null, - }, - }, - 'GET /api/apm/services/{serviceName}/annotation/search': { - annotations: [], - }, - 'GET /internal/apm/fallback_to_transactions': { - fallbackToTransactions: false, - }, - }; - /* eslint-enable @typescript-eslint/naming-convention */ - - const callApmApiSpy = getCallApmApiSpy().mockImplementation( - ({ endpoint }) => { - const response = calls[endpoint as keyof typeof calls]; - - return response - ? Promise.resolve(response) - : Promise.reject(`Response for ${endpoint} is not defined`); - } - ); - - getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any); - jest - .spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown') - .mockReturnValue({ - data: { timeseries: [] }, - error: undefined, - status: FETCH_STATUS.SUCCESS, - }); - - const { findAllByText } = renderWithTheme(, { - wrapper: Wrapper, - }); - - await waitFor(() => { - const endpoints = callApmApiSpy.mock.calls.map( - (call) => call[0].endpoint - ); - return isEqual(endpoints.sort(), Object.keys(calls).sort()); - }); + render(); - expect((await findAllByText('Latency')).length).toBeGreaterThan(0); + expect( + await screen.findByRole('heading', { name: /Latency/ }) + ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 9d207eee2fbaa..c99ef519f9e69 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -23,14 +23,20 @@ export type APMServiceAlert = ValuesType< APIReturnType<'GET /internal/apm/services/{serviceName}/alerts'>['alerts'] >; -export const APMServiceContext = createContext<{ +export interface APMServiceContextValue { serviceName: string; agentName?: string; transactionType?: string; transactionTypes: string[]; alerts: APMServiceAlert[]; runtimeName?: string; -}>({ serviceName: '', transactionTypes: [], alerts: [] }); +} + +export const APMServiceContext = createContext({ + serviceName: '', + transactionTypes: [], + alerts: [], +}); export function ApmServiceContextProvider({ children,