chore: api monitoring tests (#7750)

* feat: added url sharing for main domain list page api monitoring

* feat: added shivanshus suggestions in qb payloads for spanid and kind string client filter

* fix: limited the endpoints table limit to 1000

* feat: date picker in domain details drawer

* feat: added top errors tab in domain details

* fix: removed console logs

* feat: new dep services top 10 errors localised date picker agrregate domain details etc

* feat: added domain level and endpoint level stats

* feat: added custom cell rendering in gridcard, added new table view in all endpoints

* feat: added port column in endpoints table

* feat: added custom title handling in gridtablecomponent

* fix: fixed the traces corelation query for status code bar charts

* feat: added zoom functionality on domain details charts

* chore: add constants for standardisation

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: add constants for standardisation in the API

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: add tooltip to Endpoint Overview

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: api monitoring feedback till 28th april

* feat: added top errors to traces corelation

* feat: added new rate col to status code table

* feat: custom color mapping for uplot tooltip implemented

* chore: added ApiMonitoringPage.test

* chore: added uts for all endpoints, top errors and their utils

* fix: minor fix

* chore: moved test files to proper folder

* chore: added endpoint details uts and its imported utils ut

* chore: added endpoint dropdown uts and its imported utils ut

* chore: added endpoint metrics uts and its imported utils ut

* chore: added dependent services uts and its imported utils ut

* chore: added status code bar chart uts and its imported utils ut

* chore: added status code table uts and its imported utils ut
This commit is contained in:
Sahil Khan 2025-04-29 13:50:59 +05:30
parent 958924befe
commit ccf26883c4
11 changed files with 4028 additions and 20 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
import { fireEvent, render, screen } from '@testing-library/react';
import {
getAllEndpointsWidgetData,
getGroupByFiltersFromGroupByValues,
} from 'container/ApiMonitoring/utils';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import AllEndPoints from '../Explorer/Domains/DomainDetails/AllEndPoints';
import {
SPAN_ATTRIBUTES,
VIEWS,
} from '../Explorer/Domains/DomainDetails/constants';
// Mock the dependencies
jest.mock('container/ApiMonitoring/utils', () => ({
getAllEndpointsWidgetData: jest.fn(),
getGroupByFiltersFromGroupByValues: jest.fn(),
}));
jest.mock('container/GridCardLayout/GridCard', () => ({
__esModule: true,
default: jest.fn().mockImplementation(({ customOnRowClick }) => (
<div data-testid="grid-card-mock">
<button
type="button"
data-testid="row-click-button"
onClick={(): void =>
customOnRowClick({ [SPAN_ATTRIBUTES.URL_PATH]: '/api/test' })
}
>
Click Row
</button>
</div>
)),
}));
jest.mock(
'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2',
() => ({
__esModule: true,
default: jest.fn().mockImplementation(({ onChange }) => (
<div data-testid="query-builder-mock">
<button
type="button"
data-testid="filter-change-button"
onClick={(): void =>
onChange({
items: [{ id: 'test', key: 'test', op: '=', value: 'test' }],
op: 'AND',
})
}
>
Change Filter
</button>
</div>
)),
}),
);
jest.mock('hooks/queryBuilder/useGetAggregateKeys', () => ({
useGetAggregateKeys: jest.fn(),
}));
jest.mock('antd', () => {
const originalModule = jest.requireActual('antd');
return {
...originalModule,
Select: jest.fn().mockImplementation(({ onChange }) => (
<div data-testid="select-mock">
<button
data-testid="select-change-button"
type="button"
onClick={(): void => onChange(['http.status_code'])}
>
Change GroupBy
</button>
</div>
)),
};
});
describe('AllEndPoints', () => {
const mockProps = {
domainName: 'test-domain',
setSelectedEndPointName: jest.fn(),
setSelectedView: jest.fn(),
groupBy: [],
setGroupBy: jest.fn(),
timeRange: {
startTime: 1609459200000,
endTime: 1609545600000,
},
initialFilters: { op: 'AND', items: [] },
setInitialFiltersEndPointStats: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
// Setup mock implementations
(useGetAggregateKeys as jest.Mock).mockReturnValue({
data: {
payload: {
attributeKeys: [
{
key: 'http.status_code',
dataType: 'string',
isColumn: true,
isJSON: false,
type: '',
},
],
},
},
isLoading: false,
});
(getAllEndpointsWidgetData as jest.Mock).mockReturnValue({
id: 'test-widget',
title: 'Endpoint Overview',
description: 'Endpoint Overview',
panelTypes: 'table',
queryData: [],
});
(getGroupByFiltersFromGroupByValues as jest.Mock).mockReturnValue({
items: [{ id: 'group-filter', key: 'status', op: '=', value: '200' }],
op: 'AND',
});
});
it('renders component correctly', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<AllEndPoints {...mockProps} />);
// Verify basic component rendering
expect(screen.getByText('Group by')).toBeInTheDocument();
expect(screen.getByTestId('query-builder-mock')).toBeInTheDocument();
expect(screen.getByTestId('select-mock')).toBeInTheDocument();
expect(screen.getByTestId('grid-card-mock')).toBeInTheDocument();
});
it('handles filter changes', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<AllEndPoints {...mockProps} />);
// Trigger filter change
fireEvent.click(screen.getByTestId('filter-change-button'));
// Check if getAllEndpointsWidgetData was called with updated filters
expect(getAllEndpointsWidgetData).toHaveBeenCalledWith(
expect.anything(),
'test-domain',
expect.objectContaining({
items: expect.arrayContaining([expect.objectContaining({ id: 'test' })]),
op: 'AND',
}),
);
});
it('handles group by changes', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<AllEndPoints {...mockProps} />);
// Trigger group by change
fireEvent.click(screen.getByTestId('select-change-button'));
// Check if setGroupBy was called with updated group by value
expect(mockProps.setGroupBy).toHaveBeenCalled();
});
it('handles row click in grid card', async () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<AllEndPoints {...mockProps} />);
// Trigger row click
fireEvent.click(screen.getByTestId('row-click-button'));
// Check if proper functions were called
expect(mockProps.setSelectedEndPointName).toHaveBeenCalledWith('/api/test');
expect(mockProps.setSelectedView).toHaveBeenCalledWith(VIEWS.ENDPOINT_STATS);
expect(mockProps.setInitialFiltersEndPointStats).toHaveBeenCalled();
expect(getGroupByFiltersFromGroupByValues).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,366 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils';
import { SuccessResponse } from 'types/api';
import DependentServices from '../Explorer/Domains/DomainDetails/components/DependentServices';
import ErrorState from '../Explorer/Domains/DomainDetails/components/ErrorState';
// Create a partial mock of the UseQueryResult interface for testing
interface MockQueryResult {
isLoading: boolean;
isRefetching: boolean;
isError: boolean;
data?: any;
refetch: () => void;
}
// Mock the utility function
jest.mock('container/ApiMonitoring/utils', () => ({
getFormattedDependentServicesData: jest.fn(),
dependentServicesColumns: [
{ title: 'Dependent Services', dataIndex: 'serviceData', key: 'serviceData' },
{ title: 'AVG. LATENCY', dataIndex: 'latency', key: 'latency' },
{ title: 'ERROR %', dataIndex: 'errorPercentage', key: 'errorPercentage' },
{ title: 'AVG. RATE', dataIndex: 'rate', key: 'rate' },
],
}));
// Mock the ErrorState component
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () => ({
__esModule: true,
default: jest.fn().mockImplementation(({ refetch }) => (
<div data-testid="error-state-mock">
<button type="button" data-testid="refetch-button" onClick={refetch}>
Retry
</button>
</div>
)),
}));
// Mock antd components
jest.mock('antd', () => {
const originalModule = jest.requireActual('antd');
return {
...originalModule,
Table: jest
.fn()
.mockImplementation(({ dataSource, loading, pagination, onRow }) => (
<div data-testid="table-mock">
<div data-testid="loading-state">
{loading ? 'Loading' : 'Not Loading'}
</div>
<div data-testid="row-count">{dataSource?.length || 0}</div>
<div data-testid="page-size">{pagination?.pageSize}</div>
{dataSource?.map((item: any, index: number) => (
<div
key={`service-${item.key || index}`}
data-testid={`table-row-${index}`}
onClick={(): void => onRow?.(item)?.onClick?.()}
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>): void => {
if (e.key === 'Enter' || e.key === ' ') {
onRow?.(item)?.onClick?.();
}
}}
role="button"
tabIndex={0}
>
{item.serviceData.serviceName}
</div>
))}
</div>
)),
Skeleton: jest
.fn()
.mockImplementation(() => <div data-testid="skeleton-mock" />),
Typography: {
Text: jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="typography-text">{children}</div>
)),
},
};
});
describe('DependentServices', () => {
// Sample mock data to use in tests
const mockDependentServicesData = [
{
key: 'service1',
serviceData: {
// eslint-disable-next-line sonarjs/no-duplicate-string
serviceName: 'auth-service',
count: 500,
percentage: 62.5,
},
latency: 120,
rate: '15',
errorPercentage: '2.5',
},
{
key: 'service2',
serviceData: {
serviceName: 'db-service',
count: 300,
percentage: 37.5,
},
latency: 80,
rate: '10',
errorPercentage: '1.2',
},
];
// Default props for tests
const mockTimeRange = {
startTime: 1609459200000,
endTime: 1609545600000,
};
const refetchFn = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(getFormattedDependentServicesData as jest.Mock).mockReturnValue(
mockDependentServicesData,
);
});
it('renders loading state correctly', () => {
// Arrange
const mockQuery: MockQueryResult = {
isLoading: true,
isRefetching: false,
isError: false,
data: undefined,
refetch: refetchFn,
};
// Act
const { container } = render(
<DependentServices
dependentServicesQuery={mockQuery as any}
timeRange={mockTimeRange}
/>,
);
// Assert
expect(container.querySelector('.ant-skeleton')).toBeInTheDocument();
});
it('renders error state correctly', () => {
// Arrange
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: true,
data: undefined,
refetch: refetchFn,
};
// Act
render(
<DependentServices
dependentServicesQuery={mockQuery as any}
timeRange={mockTimeRange}
/>,
);
// Assert
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
expect(ErrorState).toHaveBeenCalledWith(
{ refetch: expect.any(Function) },
expect.anything(),
);
});
it('renders data correctly when loaded', () => {
// Arrange
const mockData = {
payload: {
data: {
result: [
{
table: {
rows: [
{
data: {
'service.name': 'auth-service',
A: '500',
B: '120000000',
C: '15',
F1: '2.5',
},
},
],
},
},
],
},
},
} as SuccessResponse<any>;
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
// Act
render(
<DependentServices
dependentServicesQuery={mockQuery as any}
timeRange={mockTimeRange}
/>,
);
// Assert
expect(getFormattedDependentServicesData).toHaveBeenCalledWith(
mockData.payload.data.result[0].table.rows,
);
// Check the table was rendered with the correct data
expect(screen.getByTestId('table-mock')).toBeInTheDocument();
expect(screen.getByTestId('loading-state')).toHaveTextContent('Not Loading');
expect(screen.getByTestId('row-count')).toHaveTextContent('2');
// Default (collapsed) pagination should be 5
expect(screen.getByTestId('page-size')).toHaveTextContent('5');
});
it('handles refetching state correctly', () => {
// Arrange
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: true,
isError: false,
data: undefined,
refetch: refetchFn,
};
// Act
const { container } = render(
<DependentServices
dependentServicesQuery={mockQuery as any}
timeRange={mockTimeRange}
/>,
);
// Assert
expect(container.querySelector('.ant-skeleton')).toBeInTheDocument();
});
it('handles row click correctly', () => {
// Mock window.open
const originalOpen = window.open;
window.open = jest.fn();
// Arrange
const mockData = {
payload: {
data: {
result: [
{
table: {
rows: [
{
data: {
'service.name': 'auth-service',
A: '500',
B: '120000000',
C: '15',
F1: '2.5',
},
},
],
},
},
],
},
},
} as SuccessResponse<any>;
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
// Act
render(
<DependentServices
dependentServicesQuery={mockQuery as any}
timeRange={mockTimeRange}
/>,
);
// Click on the first row
fireEvent.click(screen.getByTestId('table-row-0'));
// Assert
expect(window.open).toHaveBeenCalledWith(
expect.stringContaining('/services/auth-service'),
'_blank',
);
// Restore original window.open
window.open = originalOpen;
});
it('expands table when showing more', () => {
// Set up more than 5 items so the "show more" button appears
const moreItems = Array(8)
.fill(0)
.map((_, index) => ({
key: `service${index}`,
serviceData: {
serviceName: `service-${index}`,
count: 100,
percentage: 12.5,
},
latency: 100,
rate: '10',
errorPercentage: '1',
}));
(getFormattedDependentServicesData as jest.Mock).mockReturnValue(moreItems);
const mockData = {
payload: { data: { result: [{ table: { rows: [] } }] } },
} as SuccessResponse<any>;
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
// Render the component
render(
<DependentServices
dependentServicesQuery={mockQuery as any}
timeRange={mockTimeRange}
/>,
);
// Find the "Show more" button (using container query since it might not have a testId)
const showMoreButton = screen.getByText(/Show more/i);
expect(showMoreButton).toBeInTheDocument();
// Initial page size should be 5
expect(screen.getByTestId('page-size')).toHaveTextContent('5');
// Click the button to expand
fireEvent.click(showMoreButton);
// Page size should now be the full data length
expect(screen.getByTestId('page-size')).toHaveTextContent('8');
// Text should have changed to "Show less"
expect(screen.getByText(/Show less/i)).toBeInTheDocument();
});
});

View File

@ -0,0 +1,386 @@
import { fireEvent, render, screen } from '@testing-library/react';
import {
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
extractPortAndEndpoint,
getEndPointDetailsQueryPayload,
getLatencyOverTimeWidgetData,
getRateOverTimeWidgetData,
} from 'container/ApiMonitoring/utils';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueries } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { SPAN_ATTRIBUTES } from '../Explorer/Domains/DomainDetails/constants';
import EndPointDetails from '../Explorer/Domains/DomainDetails/EndPointDetails';
// Mock dependencies
jest.mock('react-query', () => ({
useQueries: jest.fn(),
}));
jest.mock('container/ApiMonitoring/utils', () => ({
END_POINT_DETAILS_QUERY_KEYS_ARRAY: [
'endPointMetricsData',
'endPointStatusCodeData',
'endPointDropDownData',
'endPointDependentServicesData',
'endPointStatusCodeBarChartsData',
'endPointStatusCodeLatencyBarChartsData',
],
extractPortAndEndpoint: jest.fn(),
getEndPointDetailsQueryPayload: jest.fn(),
getLatencyOverTimeWidgetData: jest.fn(),
getRateOverTimeWidgetData: jest.fn(),
}));
jest.mock(
'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2',
() => ({
__esModule: true,
default: jest.fn().mockImplementation(({ onChange }) => (
<div data-testid="query-builder-search">
<button
type="button"
data-testid="filter-change-button"
onClick={(): void =>
onChange({
items: [
{
id: 'test-filter',
key: {
key: 'test.key',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
op: '=',
value: 'test-value',
},
],
op: 'AND',
})
}
>
Change Filter
</button>
</div>
)),
}),
);
// Mock all child components to simplify testing
jest.mock(
'../Explorer/Domains/DomainDetails/components/EndPointMetrics',
() => ({
__esModule: true,
default: jest
.fn()
.mockImplementation(() => (
<div data-testid="endpoint-metrics">EndPoint Metrics</div>
)),
}),
);
jest.mock(
'../Explorer/Domains/DomainDetails/components/EndPointsDropDown',
() => ({
__esModule: true,
default: jest.fn().mockImplementation(({ setSelectedEndPointName }) => (
<div data-testid="endpoints-dropdown">
<button
type="button"
data-testid="select-endpoint-button"
onClick={(): void => setSelectedEndPointName('/api/new-endpoint')}
>
Select Endpoint
</button>
</div>
)),
}),
);
jest.mock(
'../Explorer/Domains/DomainDetails/components/DependentServices',
() => ({
__esModule: true,
default: jest
.fn()
.mockImplementation(() => (
<div data-testid="dependent-services">Dependent Services</div>
)),
}),
);
jest.mock(
'../Explorer/Domains/DomainDetails/components/StatusCodeBarCharts',
() => ({
__esModule: true,
default: jest
.fn()
.mockImplementation(() => (
<div data-testid="status-code-bar-charts">Status Code Bar Charts</div>
)),
}),
);
jest.mock(
'../Explorer/Domains/DomainDetails/components/StatusCodeTable',
() => ({
__esModule: true,
default: jest
.fn()
.mockImplementation(() => (
<div data-testid="status-code-table">Status Code Table</div>
)),
}),
);
jest.mock(
'../Explorer/Domains/DomainDetails/components/MetricOverTimeGraph',
() => ({
__esModule: true,
default: jest
.fn()
.mockImplementation(({ widget }) => (
<div data-testid={`metric-graph-${widget.title}`}>{widget.title} Graph</div>
)),
}),
);
describe('EndPointDetails Component', () => {
const mockQueryResults = Array(6).fill({
data: { data: [] },
isLoading: false,
isError: false,
error: null,
});
const mockProps = {
// eslint-disable-next-line sonarjs/no-duplicate-string
domainName: 'test-domain',
endPointName: '/api/test',
setSelectedEndPointName: jest.fn(),
initialFilters: { items: [], op: 'AND' } as TagFilter,
timeRange: {
startTime: 1609459200000,
endTime: 1609545600000,
},
handleTimeChange: jest.fn() as (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void,
};
beforeEach(() => {
jest.clearAllMocks();
(extractPortAndEndpoint as jest.Mock).mockReturnValue({
port: '8080',
endpoint: '/api/test',
});
(getEndPointDetailsQueryPayload as jest.Mock).mockReturnValue([
{ id: 'query1', label: 'Query 1' },
{ id: 'query2', label: 'Query 2' },
{ id: 'query3', label: 'Query 3' },
{ id: 'query4', label: 'Query 4' },
{ id: 'query5', label: 'Query 5' },
{ id: 'query6', label: 'Query 6' },
]);
(getRateOverTimeWidgetData as jest.Mock).mockReturnValue({
title: 'Rate Over Time',
id: 'rate-widget',
});
(getLatencyOverTimeWidgetData as jest.Mock).mockReturnValue({
title: 'Latency Over Time',
id: 'latency-widget',
});
(useQueries as jest.Mock).mockReturnValue(mockQueryResults);
});
it('renders the component correctly', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...mockProps} />);
// Check all major components are rendered
expect(screen.getByTestId('query-builder-search')).toBeInTheDocument();
expect(screen.getByTestId('endpoints-dropdown')).toBeInTheDocument();
expect(screen.getByTestId('endpoint-metrics')).toBeInTheDocument();
expect(screen.getByTestId('dependent-services')).toBeInTheDocument();
expect(screen.getByTestId('status-code-bar-charts')).toBeInTheDocument();
expect(screen.getByTestId('status-code-table')).toBeInTheDocument();
expect(screen.getByTestId('metric-graph-Rate Over Time')).toBeInTheDocument();
expect(
screen.getByTestId('metric-graph-Latency Over Time'),
).toBeInTheDocument();
// Check endpoint metadata is displayed
expect(screen.getByText(/8080/i)).toBeInTheDocument();
expect(screen.getByText('/api/test')).toBeInTheDocument();
});
it('calls getEndPointDetailsQueryPayload with correct parameters', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...mockProps} />);
expect(getEndPointDetailsQueryPayload).toHaveBeenCalledWith(
'test-domain',
mockProps.timeRange.startTime,
mockProps.timeRange.endTime,
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
value: '/api/test',
}),
]),
op: 'AND',
}),
);
});
it('adds endpoint filter to initial filters', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...mockProps} />);
expect(getEndPointDetailsQueryPayload).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
value: '/api/test',
}),
]),
}),
);
});
it('updates filters when QueryBuilderSearch changes', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...mockProps} />);
// Trigger filter change
fireEvent.click(screen.getByTestId('filter-change-button'));
// Check that filters were updated in subsequent calls to utility functions
expect(getEndPointDetailsQueryPayload).toHaveBeenCalledTimes(2);
expect(getEndPointDetailsQueryPayload).toHaveBeenLastCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: 'test.key' }),
value: 'test-value',
}),
]),
}),
);
});
it('handles endpoint dropdown selection', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...mockProps} />);
// Trigger endpoint selection
fireEvent.click(screen.getByTestId('select-endpoint-button'));
// Check if endpoint was updated
expect(mockProps.setSelectedEndPointName).toHaveBeenCalledWith(
'/api/new-endpoint',
);
});
it('does not display dependent services when service filter is applied', () => {
const propsWithServiceFilter = {
...mockProps,
initialFilters: {
items: [
{
id: 'service-filter',
key: {
key: 'service.name',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
op: '=',
value: 'test-service',
},
] as TagFilterItem[],
op: 'AND',
} as TagFilter,
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...propsWithServiceFilter} />);
// Dependent services should not be displayed
expect(screen.queryByTestId('dependent-services')).not.toBeInTheDocument();
});
it('passes the correct parameters to widget data generators', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...mockProps} />);
expect(getRateOverTimeWidgetData).toHaveBeenCalledWith(
'test-domain',
'/api/test',
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
value: '/api/test',
}),
]),
}),
);
expect(getLatencyOverTimeWidgetData).toHaveBeenCalledWith(
'test-domain',
'/api/test',
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: SPAN_ATTRIBUTES.URL_PATH }),
value: '/api/test',
}),
]),
}),
);
});
it('generates correct query parameters for useQueries', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointDetails {...mockProps} />);
// Check if useQueries was called with correct parameters
expect(useQueries).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
queryKey: expect.arrayContaining([END_POINT_DETAILS_QUERY_KEYS_ARRAY[0]]),
}),
expect.objectContaining({
queryKey: expect.arrayContaining([END_POINT_DETAILS_QUERY_KEYS_ARRAY[1]]),
}),
// ... and so on for other queries
]),
);
});
});

View File

@ -0,0 +1,211 @@
import { render, screen } from '@testing-library/react';
import { getFormattedEndPointMetricsData } from 'container/ApiMonitoring/utils';
import { SuccessResponse } from 'types/api';
import EndPointMetrics from '../Explorer/Domains/DomainDetails/components/EndPointMetrics';
import ErrorState from '../Explorer/Domains/DomainDetails/components/ErrorState';
// Create a partial mock of the UseQueryResult interface for testing
interface MockQueryResult {
isLoading: boolean;
isRefetching: boolean;
isError: boolean;
data?: any;
refetch: () => void;
}
// Mock the utils function
jest.mock('container/ApiMonitoring/utils', () => ({
getFormattedEndPointMetricsData: jest.fn(),
}));
// Mock the ErrorState component
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () => ({
__esModule: true,
default: jest.fn().mockImplementation(({ refetch }) => (
<div data-testid="error-state-mock">
<button type="button" data-testid="refetch-button" onClick={refetch}>
Retry
</button>
</div>
)),
}));
// Mock antd components
jest.mock('antd', () => {
const originalModule = jest.requireActual('antd');
return {
...originalModule,
Progress: jest
.fn()
.mockImplementation(() => <div data-testid="progress-bar-mock" />),
Skeleton: {
Button: jest
.fn()
.mockImplementation(() => <div data-testid="skeleton-button-mock" />),
},
Tooltip: jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="tooltip-mock">{children}</div>
)),
Typography: {
Text: jest.fn().mockImplementation(({ children, className }) => (
<div data-testid={`typography-${className}`} className={className}>
{children}
</div>
)),
},
};
});
describe('EndPointMetrics', () => {
// Common metric data to use in tests
const mockMetricsData = {
key: 'test-key',
rate: '42',
latency: 99,
errorRate: 5.5,
lastUsed: '5 minutes ago',
};
// Basic props for tests
const refetchFn = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(getFormattedEndPointMetricsData as jest.Mock).mockReturnValue(
mockMetricsData,
);
});
it('renders loading state correctly', () => {
const mockQuery: MockQueryResult = {
isLoading: true,
isRefetching: false,
isError: false,
data: undefined,
refetch: refetchFn,
};
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
// Verify skeleton loaders are visible
const skeletonElements = screen.getAllByTestId('skeleton-button-mock');
expect(skeletonElements.length).toBe(4);
// Verify labels are visible even during loading
expect(screen.getByText('Rate')).toBeInTheDocument();
expect(screen.getByText('AVERAGE LATENCY')).toBeInTheDocument();
expect(screen.getByText('ERROR %')).toBeInTheDocument();
expect(screen.getByText('LAST USED')).toBeInTheDocument();
});
it('renders error state correctly', () => {
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: true,
data: undefined,
refetch: refetchFn,
};
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
// Verify error state is shown
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
expect(ErrorState).toHaveBeenCalledWith(
{ refetch: expect.any(Function) },
expect.anything(),
);
});
it('renders data correctly when loaded', () => {
const mockData = {
payload: {
data: {
result: [
{
table: {
rows: [
{ data: { A: '42', B: '99000000', D: '1609459200000000', F1: '5.5' } },
],
},
},
],
},
},
} as SuccessResponse<any>;
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
// Verify the utils function was called with the data
expect(getFormattedEndPointMetricsData).toHaveBeenCalledWith(
mockData.payload.data.result[0].table.rows,
);
// Verify data is displayed
expect(
screen.getByText(`${mockMetricsData.rate} ops/sec`),
).toBeInTheDocument();
expect(screen.getByText(`${mockMetricsData.latency}ms`)).toBeInTheDocument();
expect(screen.getByText(mockMetricsData.lastUsed)).toBeInTheDocument();
expect(screen.getByTestId('progress-bar-mock')).toBeInTheDocument(); // For error rate
});
it('handles refetching state correctly', () => {
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: true,
isError: false,
data: undefined,
refetch: refetchFn,
};
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
// Verify skeleton loaders are visible during refetching
const skeletonElements = screen.getAllByTestId('skeleton-button-mock');
expect(skeletonElements.length).toBe(4);
});
it('handles null metrics data gracefully', () => {
// Mock the utils function to return null to simulate missing data
(getFormattedEndPointMetricsData as jest.Mock).mockReturnValue(null);
const mockData = {
payload: {
data: {
result: [
{
table: {
rows: [],
},
},
],
},
},
} as SuccessResponse<any>;
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
render(<EndPointMetrics endPointMetricsDataQuery={mockQuery as any} />);
// Even with null data, the component should render without crashing
expect(screen.getByText('Rate')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,221 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { getFormattedEndPointDropDownData } from 'container/ApiMonitoring/utils';
import EndPointsDropDown from '../Explorer/Domains/DomainDetails/components/EndPointsDropDown';
import { SPAN_ATTRIBUTES } from '../Explorer/Domains/DomainDetails/constants';
// Mock the Select component from antd
jest.mock('antd', () => {
const originalModule = jest.requireActual('antd');
return {
...originalModule,
Select: jest
.fn()
.mockImplementation(({ value, loading, onChange, options, onClear }) => (
<div data-testid="mock-select">
<div data-testid="select-value">{value}</div>
<div data-testid="select-loading">
{loading ? 'loading' : 'not-loading'}
</div>
<select
data-testid="select-element"
value={value || ''}
onChange={(e): void => onChange(e.target.value)}
>
<option value="">Select...</option>
{options?.map((option: { value: string; label: string; key: string }) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<button data-testid="select-clear-button" type="button" onClick={onClear}>
Clear
</button>
</div>
)),
};
});
// Mock the utilities
jest.mock('container/ApiMonitoring/utils', () => ({
getFormattedEndPointDropDownData: jest.fn(),
}));
describe('EndPointsDropDown Component', () => {
const mockEndPoints = [
// eslint-disable-next-line sonarjs/no-duplicate-string
{ key: '1', value: '/api/endpoint1', label: '/api/endpoint1' },
// eslint-disable-next-line sonarjs/no-duplicate-string
{ key: '2', value: '/api/endpoint2', label: '/api/endpoint2' },
];
const mockSetSelectedEndPointName = jest.fn();
// Create a mock that satisfies the UseQueryResult interface
const createMockQueryResult = (overrides: any = {}): any => ({
data: {
payload: {
data: {
result: [
{
table: {
rows: [],
},
},
],
},
},
},
dataUpdatedAt: 0,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
isError: false,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isIdle: false,
isLoading: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isRefetching: false,
isStale: false,
isSuccess: true,
refetch: jest.fn(),
remove: jest.fn(),
status: 'success',
...overrides,
});
const defaultProps = {
selectedEndPointName: '',
setSelectedEndPointName: mockSetSelectedEndPointName,
endPointDropDownDataQuery: createMockQueryResult(),
};
beforeEach(() => {
jest.clearAllMocks();
(getFormattedEndPointDropDownData as jest.Mock).mockReturnValue(
mockEndPoints,
);
});
it('renders the component correctly', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...defaultProps} />);
expect(screen.getByTestId('mock-select')).toBeInTheDocument();
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByTestId('select-loading')).toHaveTextContent('not-loading');
});
it('shows loading state when data is loading', () => {
const loadingProps = {
...defaultProps,
endPointDropDownDataQuery: createMockQueryResult({
isLoading: true,
}),
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...loadingProps} />);
expect(screen.getByTestId('select-loading')).toHaveTextContent('loading');
});
it('shows loading state when data is fetching', () => {
const fetchingProps = {
...defaultProps,
endPointDropDownDataQuery: createMockQueryResult({
isFetching: true,
}),
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...fetchingProps} />);
expect(screen.getByTestId('select-loading')).toHaveTextContent('loading');
});
it('displays the selected endpoint', () => {
const selectedProps = {
...defaultProps,
selectedEndPointName: '/api/endpoint1',
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...selectedProps} />);
expect(screen.getByTestId('select-value')).toHaveTextContent(
'/api/endpoint1',
);
});
it('calls setSelectedEndPointName when an option is selected', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...defaultProps} />);
// Get the select element and change its value
const selectElement = screen.getByTestId('select-element');
fireEvent.change(selectElement, { target: { value: '/api/endpoint2' } });
expect(mockSetSelectedEndPointName).toHaveBeenCalledWith('/api/endpoint2');
});
it('calls setSelectedEndPointName with empty string when cleared', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...defaultProps} />);
// Click the clear button
const clearButton = screen.getByTestId('select-clear-button');
fireEvent.click(clearButton);
expect(mockSetSelectedEndPointName).toHaveBeenCalledWith('');
});
it('passes dropdown style prop correctly', () => {
const styleProps = {
...defaultProps,
dropdownStyle: { maxHeight: '200px' },
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...styleProps} />);
// We can't easily test style props in our mock, but at least ensure the component rendered
expect(screen.getByTestId('mock-select')).toBeInTheDocument();
});
it('formats data using the utility function', () => {
const mockRows = [
{ data: { [SPAN_ATTRIBUTES.URL_PATH]: '/api/test', A: 10 } },
];
const dataProps = {
...defaultProps,
endPointDropDownDataQuery: createMockQueryResult({
data: {
payload: {
data: {
result: [
{
table: {
rows: mockRows,
},
},
],
},
},
},
}),
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<EndPointsDropDown {...dataProps} />);
expect(getFormattedEndPointDropDownData).toHaveBeenCalledWith(mockRows);
});
});

View File

@ -0,0 +1,493 @@
import { fireEvent, render, screen } from '@testing-library/react';
import {
getCustomFiltersForBarChart,
getFormattedEndPointStatusCodeChartData,
getStatusCodeBarChartWidgetData,
} from 'container/ApiMonitoring/utils';
import { SuccessResponse } from 'types/api';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import ErrorState from '../Explorer/Domains/DomainDetails/components/ErrorState';
import StatusCodeBarCharts from '../Explorer/Domains/DomainDetails/components/StatusCodeBarCharts';
// Create a partial mock of the UseQueryResult interface for testing
interface MockQueryResult {
isLoading: boolean;
isRefetching: boolean;
isError: boolean;
error?: Error;
data?: any;
refetch: () => void;
}
// Mocks
jest.mock('components/Uplot', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => <div data-testid="uplot-mock" />),
}));
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
getCustomSeries: jest.fn(),
}),
}));
jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
useNavigateToExplorer: (): { navigateToExplorer: jest.Mock } => ({
navigateToExplorer: jest.fn(),
}),
}));
jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
useGraphClickToShowButton: (): {
componentClick: boolean;
htmlRef: HTMLElement | null;
} => ({
componentClick: false,
htmlRef: null,
}),
}));
jest.mock('container/GridCardLayout/useNavigateToExplorerPages', () => ({
__esModule: true,
default: (): { navigateToExplorerPages: jest.Mock } => ({
navigateToExplorerPages: jest.fn(),
}),
}));
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('hooks/useDimensions', () => ({
useResizeObserver: (): { width: number; height: number } => ({
width: 800,
height: 400,
}),
}));
jest.mock('hooks/useNotifications', () => ({
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
}));
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
getUPlotChartOptions: jest.fn().mockReturnValue({}),
}));
jest.mock('lib/uPlotLib/utils/getUplotChartData', () => ({
getUPlotChartData: jest.fn().mockReturnValue([]),
}));
// Mock utility functions
jest.mock('container/ApiMonitoring/utils', () => ({
getFormattedEndPointStatusCodeChartData: jest.fn(),
getStatusCodeBarChartWidgetData: jest.fn(),
getCustomFiltersForBarChart: jest.fn(),
statusCodeWidgetInfo: [
{ title: 'Status Code Count', yAxisUnit: 'count' },
{ title: 'Status Code Latency', yAxisUnit: 'ms' },
],
}));
// Mock the ErrorState component
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () => ({
__esModule: true,
default: jest.fn().mockImplementation(({ refetch }) => (
<div data-testid="error-state-mock">
<button type="button" data-testid="refetch-button" onClick={refetch}>
Retry
</button>
</div>
)),
}));
// Mock antd components
jest.mock('antd', () => {
const originalModule = jest.requireActual('antd');
return {
...originalModule,
Card: jest.fn().mockImplementation(({ children, className }) => (
<div data-testid="card-mock" className={className}>
{children}
</div>
)),
Typography: {
Text: jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="typography-text">{children}</div>
)),
},
Button: {
...originalModule.Button,
Group: jest.fn().mockImplementation(({ children, className }) => (
<div data-testid="button-group" className={className}>
{children}
</div>
)),
},
Skeleton: jest
.fn()
.mockImplementation(() => (
<div data-testid="skeleton-mock">Loading skeleton...</div>
)),
};
});
describe('StatusCodeBarCharts', () => {
// Default props for tests
const mockFilters: IBuilderQuery['filters'] = { items: [], op: 'AND' };
const mockTimeRange = {
startTime: 1609459200000,
endTime: 1609545600000,
};
const mockDomainName = 'test-domain';
const mockEndPointName = '/api/test';
const onDragSelectMock = jest.fn();
const refetchFn = jest.fn();
// Mock formatted data
const mockFormattedData = {
data: {
result: [
{
values: [[1609459200, 10]],
metric: { statusCode: '200-299' },
queryName: 'A',
},
{
values: [[1609459200, 5]],
metric: { statusCode: '400-499' },
queryName: 'B',
},
],
resultType: 'matrix',
},
};
// Mock filter values
const mockStatusCodeFilters = [
{
id: 'test-id-1',
key: {
dataType: 'string',
id: 'response_status_code--string--tag--false',
isColumn: false,
isJSON: false,
key: 'response_status_code',
type: 'tag',
},
op: '>=',
value: '200',
},
{
id: 'test-id-2',
key: {
dataType: 'string',
id: 'response_status_code--string--tag--false',
isColumn: false,
isJSON: false,
key: 'response_status_code',
type: 'tag',
},
op: '<=',
value: '299',
},
];
beforeEach(() => {
jest.clearAllMocks();
(getFormattedEndPointStatusCodeChartData as jest.Mock).mockReturnValue(
mockFormattedData,
);
(getStatusCodeBarChartWidgetData as jest.Mock).mockReturnValue({
id: 'test-widget',
title: 'Status Code',
description: 'Shows status code distribution',
query: { builder: { queryData: [] } },
panelTypes: 'bar',
});
(getCustomFiltersForBarChart as jest.Mock).mockReturnValue(
mockStatusCodeFilters,
);
});
it('renders loading state correctly', () => {
// Arrange
const mockStatusCodeQuery: MockQueryResult = {
isLoading: true,
isRefetching: false,
isError: false,
data: undefined,
refetch: refetchFn,
};
const mockLatencyQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: undefined,
refetch: refetchFn,
};
// Act
render(
<StatusCodeBarCharts
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
/>,
);
// Assert
expect(screen.getByTestId('skeleton-mock')).toBeInTheDocument();
});
it('renders error state correctly', () => {
// Arrange
const mockStatusCodeQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: true,
error: new Error('Test error'),
data: undefined,
refetch: refetchFn,
};
const mockLatencyQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: undefined,
refetch: refetchFn,
};
// Act
render(
<StatusCodeBarCharts
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
/>,
);
// Assert
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
expect(ErrorState).toHaveBeenCalledWith(
{ refetch: expect.any(Function) },
expect.anything(),
);
});
it('renders chart data correctly when loaded', () => {
// Arrange
const mockData = {
payload: mockFormattedData,
} as SuccessResponse<any>;
const mockStatusCodeQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
const mockLatencyQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
// Act
render(
<StatusCodeBarCharts
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
/>,
);
// Assert
expect(getFormattedEndPointStatusCodeChartData).toHaveBeenCalledWith(
mockData.payload,
'sum',
);
expect(screen.getByTestId('uplot-mock')).toBeInTheDocument();
expect(screen.getByText('Number of calls')).toBeInTheDocument();
expect(screen.getByText('Latency')).toBeInTheDocument();
});
it('switches between number of calls and latency views', () => {
// Arrange
const mockData = {
payload: mockFormattedData,
} as SuccessResponse<any>;
const mockStatusCodeQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
const mockLatencyQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
// Act
render(
<StatusCodeBarCharts
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
/>,
);
// Initially should be showing number of calls (index 0)
const latencyButton = screen.getByText('Latency');
// Click to switch to latency view
fireEvent.click(latencyButton);
// Should now format with the latency data
expect(getFormattedEndPointStatusCodeChartData).toHaveBeenCalledWith(
mockData.payload,
'average',
);
});
it('uses getCustomFiltersForBarChart when needed', () => {
// Arrange
const mockData = {
payload: mockFormattedData,
} as SuccessResponse<any>;
const mockStatusCodeQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
const mockLatencyQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
// Act
render(
<StatusCodeBarCharts
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockFilters}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
/>,
);
// Assert
// Initially getCustomFiltersForBarChart won't be called until a graph click event
expect(getCustomFiltersForBarChart).not.toHaveBeenCalled();
// We can't easily test the graph click handler directly,
// but we've confirmed the function is mocked and ready to be tested
expect(getStatusCodeBarChartWidgetData).toHaveBeenCalledWith(
mockDomainName,
mockEndPointName,
expect.objectContaining({
items: [],
op: 'AND',
}),
);
});
it('handles widget generation with current filters', () => {
// Arrange
const mockCustomFilters = {
items: [
{
id: 'custom-filter',
key: { key: 'test-key' },
op: '=',
value: 'test-value',
},
],
op: 'AND',
};
const mockData = {
payload: mockFormattedData,
} as SuccessResponse<any>;
const mockStatusCodeQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
const mockLatencyQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: mockData,
refetch: refetchFn,
};
// Act
render(
<StatusCodeBarCharts
endPointStatusCodeBarChartsDataQuery={mockStatusCodeQuery as any}
endPointStatusCodeLatencyBarChartsDataQuery={mockLatencyQuery as any}
domainName={mockDomainName}
endPointName={mockEndPointName}
filters={mockCustomFilters as IBuilderQuery['filters']}
timeRange={mockTimeRange}
onDragSelect={onDragSelectMock}
/>,
);
// Assert widget creation was called with the correct parameters
expect(getStatusCodeBarChartWidgetData).toHaveBeenCalledWith(
mockDomainName,
mockEndPointName,
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({ id: 'custom-filter' }),
]),
op: 'AND',
}),
);
});
});

View File

@ -0,0 +1,175 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import StatusCodeTable from '../Explorer/Domains/DomainDetails/components/StatusCodeTable';
// Mock the ErrorState component
jest.mock('../Explorer/Domains/DomainDetails/components/ErrorState', () =>
jest.fn().mockImplementation(({ refetch }) => (
<div
data-testid="error-state-mock"
onClick={refetch}
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>): void => {
if (e.key === 'Enter' || e.key === ' ') {
refetch();
}
}}
role="button"
tabIndex={0}
>
Error state
</div>
)),
);
// Mock antd components
jest.mock('antd', () => {
const originalModule = jest.requireActual('antd');
return {
...originalModule,
Table: jest
.fn()
.mockImplementation(({ loading, dataSource, columns, locale }) => (
<div data-testid="table-mock">
{loading && <div data-testid="loading-indicator">Loading...</div>}
{dataSource &&
dataSource.length === 0 &&
!loading &&
locale?.emptyText && (
<div data-testid="empty-table">{locale.emptyText}</div>
)}
{dataSource && dataSource.length > 0 && (
<div data-testid="table-data">
Data loaded with {dataSource.length} rows and {columns.length} columns
</div>
)}
</div>
)),
Typography: {
Text: jest.fn().mockImplementation(({ children, className }) => (
<div data-testid="typography-text" className={className}>
{children}
</div>
)),
},
};
});
// Create a mock query result type
interface MockQueryResult {
isLoading: boolean;
isRefetching: boolean;
isError: boolean;
error?: Error;
data?: any;
refetch: () => void;
}
describe('StatusCodeTable', () => {
const refetchFn = jest.fn();
it('renders loading state correctly', () => {
// Arrange
const mockQuery: MockQueryResult = {
isLoading: true,
isRefetching: false,
isError: false,
data: undefined,
refetch: refetchFn,
};
// Act
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
// Assert
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
});
it('renders error state correctly', () => {
// Arrange
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: true,
error: new Error('Test error'),
data: undefined,
refetch: refetchFn,
};
// Act
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
// Assert
expect(screen.getByTestId('error-state-mock')).toBeInTheDocument();
});
it('renders empty state when no data is available', () => {
// Arrange
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: {
payload: {
data: {
result: [
{
table: {
rows: [],
},
},
],
},
},
},
refetch: refetchFn,
};
// Act
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
// Assert
expect(screen.getByTestId('empty-table')).toBeInTheDocument();
});
it('renders table data correctly when data is available', () => {
// Arrange
const mockData = [
{
data: {
response_status_code: '200',
A: '150', // count
B: '10000000', // latency in nanoseconds
C: '5', // rate
},
},
];
const mockQuery: MockQueryResult = {
isLoading: false,
isRefetching: false,
isError: false,
data: {
payload: {
data: {
result: [
{
table: {
rows: mockData,
},
},
],
},
},
},
refetch: refetchFn,
};
// Act
render(<StatusCodeTable endPointStatusCodeDataQuery={mockQuery as any} />);
// Assert
expect(screen.getByTestId('table-data')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,296 @@
import { fireEvent, render, screen, within } from '@testing-library/react';
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
formatTopErrorsDataForTable,
getEndPointDetailsQueryPayload,
getTopErrorsColumnsConfig,
getTopErrorsCoRelationQueryFilters,
getTopErrorsQueryPayload,
} from 'container/ApiMonitoring/utils';
import { useQueries } from 'react-query';
import { DataSource } from 'types/common/queryBuilder';
import TopErrors from '../Explorer/Domains/DomainDetails/TopErrors';
// Mock the EndPointsDropDown component to avoid issues
jest.mock(
'../Explorer/Domains/DomainDetails/components/EndPointsDropDown',
() => ({
__esModule: true,
default: jest.fn().mockImplementation(
({ setSelectedEndPointName }): JSX.Element => (
<div data-testid="endpoints-dropdown-mock">
<select
data-testid="endpoints-select"
onChange={(e): void => setSelectedEndPointName(e.target.value)}
role="combobox"
>
<option value="/api/test">/api/test</option>
<option value="/api/new-endpoint">/api/new-endpoint</option>
</select>
</div>
),
),
}),
);
// Mock dependencies
jest.mock('react-query', () => ({
useQueries: jest.fn(),
}));
jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
useNavigateToExplorer: jest.fn(),
}));
jest.mock('container/ApiMonitoring/utils', () => ({
END_POINT_DETAILS_QUERY_KEYS_ARRAY: ['key1', 'key2', 'key3', 'key4', 'key5'],
formatTopErrorsDataForTable: jest.fn(),
getEndPointDetailsQueryPayload: jest.fn(),
getTopErrorsColumnsConfig: jest.fn(),
getTopErrorsCoRelationQueryFilters: jest.fn(),
getTopErrorsQueryPayload: jest.fn(),
}));
describe('TopErrors', () => {
const mockProps = {
// eslint-disable-next-line sonarjs/no-duplicate-string
domainName: 'test-domain',
timeRange: {
startTime: 1000000000,
endTime: 1000010000,
},
handleTimeChange: jest.fn(),
};
// Setup basic mocks
beforeEach(() => {
jest.clearAllMocks();
// Mock getTopErrorsColumnsConfig
(getTopErrorsColumnsConfig as jest.Mock).mockReturnValue([
{
title: 'Endpoint',
dataIndex: 'endpointName',
key: 'endpointName',
},
{
title: 'Status Code',
dataIndex: 'statusCode',
key: 'statusCode',
},
{
title: 'Status Message',
dataIndex: 'statusMessage',
key: 'statusMessage',
},
{
title: 'Count',
dataIndex: 'count',
key: 'count',
},
]);
// Mock useQueries
(useQueries as jest.Mock).mockImplementation((queryConfigs) => {
// For topErrorsDataQueries
if (
queryConfigs.length === 1 &&
queryConfigs[0].queryKey &&
queryConfigs[0].queryKey[0] === REACT_QUERY_KEY.GET_TOP_ERRORS_BY_DOMAIN
) {
return [
{
data: {
payload: {
data: {
result: [
{
metric: {
'http.url': '/api/test',
status_code: '500',
// eslint-disable-next-line sonarjs/no-duplicate-string
status_message: 'Internal Server Error',
},
values: [[1000000100, '10']],
queryName: 'A',
legend: 'Test Legend',
},
],
},
},
},
isLoading: false,
isRefetching: false,
isError: false,
refetch: jest.fn(),
},
];
}
// For endPointDropDownDataQueries
return [
{
data: {
payload: {
data: {
result: [
{
table: {
rows: [
{
'http.url': '/api/test',
A: 100,
},
],
},
},
],
},
},
},
isLoading: false,
isRefetching: false,
isError: false,
},
];
});
// Mock formatTopErrorsDataForTable
(formatTopErrorsDataForTable as jest.Mock).mockReturnValue([
{
key: '1',
endpointName: '/api/test',
statusCode: '500',
statusMessage: 'Internal Server Error',
count: 10,
},
]);
// Mock getTopErrorsQueryPayload
(getTopErrorsQueryPayload as jest.Mock).mockReturnValue([
{
queryName: 'TopErrorsQuery',
start: mockProps.timeRange.startTime,
end: mockProps.timeRange.endTime,
step: 60,
},
]);
// Mock getEndPointDetailsQueryPayload
(getEndPointDetailsQueryPayload as jest.Mock).mockReturnValue([
{},
{},
{
queryName: 'EndpointDropdownQuery',
start: mockProps.timeRange.startTime,
end: mockProps.timeRange.endTime,
step: 60,
},
]);
// Mock useNavigateToExplorer
(useNavigateToExplorer as jest.Mock).mockReturnValue(jest.fn());
// Mock getTopErrorsCoRelationQueryFilters
(getTopErrorsCoRelationQueryFilters as jest.Mock).mockReturnValue({
items: [
{ id: 'test1', key: { key: 'domain' }, op: '=', value: 'test-domain' },
{ id: 'test2', key: { key: 'endpoint' }, op: '=', value: '/api/test' },
{ id: 'test3', key: { key: 'status' }, op: '=', value: '500' },
],
op: 'AND',
});
});
it('renders component correctly', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
const { container } = render(<TopErrors {...mockProps} />);
// Check if the title is rendered
expect(screen.getByText('Top Errors')).toBeInTheDocument();
// Find the table row and verify content
const tableBody = container.querySelector('.ant-table-tbody');
expect(tableBody).not.toBeNull();
if (tableBody) {
const row = within(tableBody as HTMLElement).getByRole('row');
expect(within(row).getByText('/api/test')).toBeInTheDocument();
expect(within(row).getByText('500')).toBeInTheDocument();
expect(within(row).getByText('Internal Server Error')).toBeInTheDocument();
}
});
it('calls handleTimeChange with 6h on mount', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<TopErrors {...mockProps} />);
expect(mockProps.handleTimeChange).toHaveBeenCalledWith('6h');
});
it('renders error state when isError is true', () => {
// Mock useQueries to return isError: true
(useQueries as jest.Mock).mockImplementationOnce(() => [
{
isError: true,
refetch: jest.fn(),
},
]);
// eslint-disable-next-line react/jsx-props-no-spreading
render(<TopErrors {...mockProps} />);
// Error state should be shown with the actual text displayed in the UI
expect(
screen.getByText('Uh-oh :/ We ran into an error.'),
).toBeInTheDocument();
expect(screen.getByText('Please refresh this panel.')).toBeInTheDocument();
expect(screen.getByText('Refresh this panel')).toBeInTheDocument();
});
it('handles row click correctly', () => {
const navigateMock = jest.fn();
(useNavigateToExplorer as jest.Mock).mockReturnValue(navigateMock);
// eslint-disable-next-line react/jsx-props-no-spreading
const { container } = render(<TopErrors {...mockProps} />);
// Find and click on the table cell containing the endpoint
const tableBody = container.querySelector('.ant-table-tbody');
expect(tableBody).not.toBeNull();
if (tableBody) {
const row = within(tableBody as HTMLElement).getByRole('row');
const cellWithEndpoint = within(row).getByText('/api/test');
fireEvent.click(cellWithEndpoint);
}
// Check if navigateToExplorer was called with correct params
expect(navigateMock).toHaveBeenCalledWith({
filters: [
{ id: 'test1', key: { key: 'domain' }, op: '=', value: 'test-domain' },
{ id: 'test2', key: { key: 'endpoint' }, op: '=', value: '/api/test' },
{ id: 'test3', key: { key: 'status' }, op: '=', value: '500' },
],
dataSource: DataSource.TRACES,
startTime: mockProps.timeRange.startTime,
endTime: mockProps.timeRange.endTime,
shouldResolveQuery: true,
});
});
it('updates endpoint filter when dropdown value changes', () => {
// eslint-disable-next-line react/jsx-props-no-spreading
render(<TopErrors {...mockProps} />);
// Find the dropdown
const dropdown = screen.getByRole('combobox');
// Mock the change
fireEvent.change(dropdown, { target: { value: '/api/new-endpoint' } });
// Check if getTopErrorsQueryPayload was called with updated parameters
expect(getTopErrorsQueryPayload).toHaveBeenCalled();
});
});

View File

@ -2802,7 +2802,18 @@ interface EndPointStatusCodeData {
export const getFormattedEndPointMetricsData = (
data: EndPointMetricsResponseRow[],
): EndPointMetricsData => ({
): EndPointMetricsData => {
if (!data || data.length === 0) {
return {
key: v4(),
rate: '-',
latency: '-',
errorRate: 0,
lastUsed: '-',
};
}
return {
key: v4(),
rate: data[0].data.A === 'n/a' || !data[0].data.A ? '-' : data[0].data.A,
latency:
@ -2815,12 +2826,14 @@ export const getFormattedEndPointMetricsData = (
data[0].data.D === 'n/a' || !data[0].data.D
? '-'
: getLastUsedRelativeTime(Math.floor(Number(data[0].data.D) / 1000000)),
});
};
};
export const getFormattedEndPointStatusCodeData = (
data: EndPointStatusCodeResponseRow[],
): EndPointStatusCodeData[] =>
data?.map((row) => ({
): EndPointStatusCodeData[] => {
if (!data) return [];
return data.map((row) => ({
key: v4(),
statusCode:
row.data.response_status_code === 'n/a' ||
@ -2834,6 +2847,7 @@ export const getFormattedEndPointStatusCodeData = (
? '-'
: Math.round(Number(row.data.B) / 1000000), // Convert from nanoseconds to milliseconds,
}));
};
export const endPointStatusCodeColumns: ColumnType<EndPointStatusCodeData>[] = [
{
@ -2916,12 +2930,14 @@ interface EndPointDropDownData {
export const getFormattedEndPointDropDownData = (
data: EndPointDropDownResponseRow[],
): EndPointDropDownData[] =>
data?.map((row) => ({
): EndPointDropDownData[] => {
if (!data) return [];
return data.map((row) => ({
key: v4(),
label: row.data[SPAN_ATTRIBUTES.URL_PATH] || '-',
value: row.data[SPAN_ATTRIBUTES.URL_PATH] || '-',
}));
};
interface DependentServicesResponseRow {
data: {
@ -3226,7 +3242,6 @@ export const groupStatusCodes = (
return [timestamp, finalValue.toString()];
});
});
// Define the order of status code ranges
const statusCodeOrder = ['200-299', '300-399', '400-499', '500-599', 'Other'];
@ -3350,7 +3365,13 @@ export const getFormattedEndPointStatusCodeChartData = (
aggregationType: 'sum' | 'average' = 'sum',
): EndPointStatusCodePayloadData => {
if (!data) {
return data;
return {
data: {
result: [],
newResult: [],
resultType: 'matrix',
},
};
}
return {
data: {

View File

@ -0,0 +1,59 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import ApiMonitoringPage from './ApiMonitoringPage';
// Mock the child component to isolate the ApiMonitoringPage logic
// We are not testing ExplorerPage here, just that ApiMonitoringPage renders it via RouteTab.
jest.mock('container/ApiMonitoring/Explorer/Explorer', () => ({
__esModule: true,
default: (): JSX.Element => <div>Mocked Explorer Page</div>,
}));
// Mock the RouteTab component
jest.mock('components/RouteTab', () => ({
__esModule: true,
default: ({
routes,
activeKey,
}: {
routes: any[];
activeKey: string;
}): JSX.Element => (
<div data-testid="route-tab">
<span>Active Key: {activeKey}</span>
{/* Render the component defined in the route for the activeKey */}
{routes.find((route) => route.key === activeKey)?.Component()}
</div>
),
}));
// Mock useLocation hook to properly return the path we're testing
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: '/api-monitoring/explorer',
}),
}));
describe('ApiMonitoringPage', () => {
it('should render the RouteTab with the Explorer tab', () => {
render(
<MemoryRouter initialEntries={['/api-monitoring/explorer']}>
<ApiMonitoringPage />
</MemoryRouter>,
);
// Check if the mock RouteTab is rendered
expect(screen.getByTestId('route-tab')).toBeInTheDocument();
// Instead of checking for the mock component, just verify the RouteTab is there
// and has the correct active key
expect(screen.getByText(/Active Key:/)).toBeInTheDocument();
// We can't test for the Explorer page being rendered right now
// but we'll verify the structure exists
});
// Add more tests here later, e.g., testing navigation if more tabs were added
});