chore: added trace filter test cases (#5451)

* feat: added trace filter test cases

* feat: added trace filter test cases - initial render

* feat: added test cases - query sync, filter section behaviour etc

* feat: deleted mock-data files

* feat: added test cases of undefined filters and items

* feat: deleted tsconfig

* feat: added clear and rest btn test cases for traces filters

* feat: added collapse and uncollapse test for traces filters
This commit is contained in:
SagarRajput-7 2024-07-22 11:05:20 +05:30 committed by GitHub
parent a2492b0135
commit 89fd3e4f55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 505 additions and 12 deletions

View File

@ -1,5 +1,4 @@
import { Typography } from 'antd';
import Card from 'antd/es/card/Card';
import { Card, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`

View File

@ -1,8 +1,7 @@
import './TraceDetails.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import { Button, Col, Typography } from 'antd';
import Sider from 'antd/es/layout/Sider';
import { Button, Col, Layout, Typography } from 'antd';
import cx from 'classnames';
import {
StyledCol,
@ -42,6 +41,8 @@ import {
INTERVAL_UNITS,
} from './utils';
const { Sider } = Layout;
function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const spanServiceColors = useMemo(
() => spanServiceNameToColorMapping(response[0].events),

View File

@ -1,5 +1,4 @@
import { Col } from 'antd';
import Card from 'antd/es/card/Card';
import { Card, Col } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`

View File

@ -0,0 +1,81 @@
export const explorerView = {
status: 'success',
data: [
{
uuid: 'test-uuid-1',
name: 'Table View',
category: '',
createdAt: '2023-08-29T18:04:10.906310033Z',
createdBy: 'test-user-1',
updatedAt: '2024-01-29T10:42:47.346331133Z',
updatedBy: 'test-user-1',
sourcePage: 'traces',
tags: [''],
compositeQuery: {
builderQueries: {
A: {
queryName: 'A',
stepInterval: 60,
dataSource: 'traces',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'component',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [
{
key: {
key: 'component',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
},
value: 'test-component',
op: '!=',
},
],
},
groupBy: [
{
key: 'component',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
},
{
key: 'client-uuid',
dataType: 'string',
type: 'resource',
isColumn: false,
isJSON: false,
},
],
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
reduceTo: 'sum',
ShiftBy: 0,
},
},
panelType: 'table',
queryType: 'builder',
},
extraData: '{"color":"#00ffd0"}',
},
],
};

View File

@ -2,6 +2,7 @@ import { rest } from 'msw';
import { billingSuccessResponse } from './__mockdata__/billing';
import { dashboardSuccessResponse } from './__mockdata__/dashboards';
import { explorerView } from './__mockdata__/explorer_views';
import { inviteUser } from './__mockdata__/invite_user';
import { licensesSuccessResponse } from './__mockdata__/licenses';
import { membersResponse } from './__mockdata__/members';
@ -55,6 +56,51 @@ export const handlers = [
const metricName = req.url.searchParams.get('metricName');
const tagKey = req.url.searchParams.get('tagKey');
const attributeKey = req.url.searchParams.get('attributeKey');
if (attributeKey === 'serviceName') {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
stringAttributeValues: [
'customer',
'demo-app',
'driver',
'frontend',
'mysql',
'redis',
'route',
'go-grpc-otel-server',
'test',
],
numberAttributeValues: null,
boolAttributeValues: null,
},
}),
);
}
if (attributeKey === 'name') {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
stringAttributeValues: [
'HTTP GET',
'HTTP GET /customer',
'HTTP GET /dispatch',
'HTTP GET /route',
],
numberAttributeValues: null,
boolAttributeValues: null,
},
}),
);
}
if (
metricName === 'signoz_calls_total' &&
tagKey === 'resource_signoz_collector_id'
@ -102,4 +148,31 @@ export const handlers = [
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
res(ctx.status(200), ctx.json(inviteUser)),
),
rest.get(
'http://localhost/api/v3/autocomplete/aggregate_attributes',
(req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: { attributeKeys: null },
}),
),
),
rest.get('http://localhost/api/v1/explorer/views', (req, res, ctx) =>
res(ctx.status(200), ctx.json(explorerView)),
),
rest.post('http://localhost/api/v1/event', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
statusCode: 200,
error: null,
payload: 'Event Processed Successfully',
}),
),
),
];

View File

@ -109,6 +109,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
className="min-max-input"
onChange={onChangeMinHandler}
value={preMin}
data-testid="min-input"
addonAfter="ms"
/>
<Input
@ -118,6 +119,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
className="min-max-input"
onChange={onChangeMaxHandler}
value={preMax}
data-testid="max-input"
addonAfter="ms"
/>
</div>

View File

@ -224,13 +224,18 @@ export function Filter(props: FilterProps): JSX.Element {
<Button
onClick={(): void => handleRun({ resetAll: true })}
className="sync-icon"
data-testid="reset-filters"
>
<SyncOutlined />
</Button>
</Tooltip>
</Flex>
<Tooltip title="Collapse" placement="right">
<Button onClick={(): void => setOpen(false)} className="arrow-icon">
<Button
onClick={(): void => setOpen(false)}
className="arrow-icon"
data-testid="toggle-filter-panel"
>
<VerticalAlignTopOutlined rotate={270} />
</Button>
</Tooltip>

View File

@ -64,7 +64,7 @@ export function Section(props: SectionProps): JSX.Element {
return (
<div>
<Divider plain className="divider" />
<div className="section-body-header">
<div className="section-body-header" data-testid={`collapse-${panelName}`}>
<Collapse
bordered={false}
className="collapseContainer"
@ -96,7 +96,11 @@ export function Section(props: SectionProps): JSX.Element {
},
]}
/>
<Button type="link" onClick={onClearHandler}>
<Button
type="link"
onClick={onClearHandler}
data-testid={`collapse-${panelName}-clearBtn`}
>
Clear All
</Button>
</div>

View File

@ -145,6 +145,7 @@ export function SectionBody(props: SectionBodyProps): JSX.Element {
key={`${type}-${item}`}
onChange={(e): void => onCheckHandler(e, item)}
checked={checkboxMatcher(item)}
data-testid={`${type}-${item}`}
>
<div className="checkbox-label">
<div className={labelClassname(item)} />

View File

@ -1,16 +1,19 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */
import userEvent from '@testing-library/user-event';
import {
initialQueriesMap,
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { render } from 'tests/test-utils';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import TracesExplorer from '..';
import { Filter } from '../Filter/Filter';
import { AllTraceFilterKeyValue } from '../Filter/filterUtils';
@ -37,6 +40,48 @@ jest.mock('uplot', () => {
};
});
jest.mock(
'container/TopNav/DateTimeSelectionV2/index.tsx',
() =>
function MockDateTimeSelection(): JSX.Element {
return <div>MockDateTimeSelection</div>;
},
);
function checkIfSectionIsOpen(
getByTestId: (testId: string) => HTMLElement,
panelName: string,
): void {
const section = getByTestId(`collapse-${panelName}`);
expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull();
}
function checkIfSectionIsNotOpen(
getByTestId: (testId: string) => HTMLElement,
panelName: string,
): void {
const section = getByTestId(`collapse-${panelName}`);
expect(section.querySelector('.ant-collapse-item-active')).toBeNull();
}
const defaultOpenSections = ['hasError', 'durationNano', 'serviceName'];
const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter(
(section) =>
![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes(
section,
),
);
async function checkForSectionContent(values: string[]): Promise<void> {
for (const val of values) {
const sectionContent = await screen.findByText(val);
await waitFor(() => expect(sectionContent).toBeInTheDocument());
}
}
const redirectWithQueryBuilderData = jest.fn();
const compositeQuery: Query = {
...initialQueriesMap.traces,
builder: {
@ -81,6 +126,157 @@ const compositeQuery: Query = {
};
describe('TracesExplorer - ', () => {
// Initial filter panel rendering
// Test the initial state like which filters section are opened, default state of duration slider, etc.
it('should render the Trace filter', async () => {
const { getByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument();
});
// Check default state of duration slider
const minDuration = getByTestId('min-input') as HTMLInputElement;
const maxDuration = getByTestId('max-input') as HTMLInputElement;
expect(minDuration).toHaveValue(null);
expect(minDuration).toHaveProperty('placeholder', '0');
expect(maxDuration).toHaveValue(null);
expect(maxDuration).toHaveProperty('placeholder', '100000000');
// Check which all filter section are opened by default
defaultOpenSections.forEach((section) =>
checkIfSectionIsOpen(getByTestId, section),
);
// Check which all filter section are closed by default
defaultClosedSections.forEach((section) =>
checkIfSectionIsNotOpen(getByTestId, section),
);
// check for the status section content
await checkForSectionContent(['Ok', 'Error']);
// check for the service name section content from API response
await checkForSectionContent([
'customer',
'demo-app',
'driver',
'frontend',
'mysql',
'redis',
'route',
'go-grpc-otel-server',
'test',
]);
});
// test the filter panel actions like opening and closing the sections, etc.
it('filter panel actions', async () => {
const { getByTestId } = render(<Filter setOpen={jest.fn()} />);
// Check if the section is closed
checkIfSectionIsNotOpen(getByTestId, 'name');
// Open the section
const name = getByTestId('collapse-name');
expect(name).toBeInTheDocument();
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
await waitFor(() => checkIfSectionIsOpen(getByTestId, 'name'));
await checkForSectionContent([
'HTTP GET',
'HTTP GET /customer',
'HTTP GET /dispatch',
'HTTP GET /route',
]);
// Close the section
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
await waitFor(() => checkIfSectionIsNotOpen(getByTestId, 'name'));
});
it('checking filters should update the query', async () => {
const { getByText } = render(
<QueryBuilderContext.Provider
value={
{
currentQuery: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [initialQueryBuilderFormValues],
},
},
redirectWithQueryBuilderData,
} as any
}
>
<Filter setOpen={jest.fn()} />
</QueryBuilderContext.Provider>,
);
const okCheckbox = getByText('Ok');
fireEvent.click(okCheckbox);
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
id: expect.any(String),
key: 'hasError',
type: 'tag',
dataType: 'bool',
isColumn: true,
isJSON: false,
},
op: 'in',
value: ['false'],
}),
]),
);
// Check if the query is updated when the error checkbox is clicked
const errorCheckbox = getByText('Error');
fireEvent.click(errorCheckbox);
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
id: expect.any(String),
key: 'hasError',
type: 'tag',
dataType: 'bool',
isColumn: true,
isJSON: false,
},
op: 'in',
value: ['false', 'true'],
}),
]),
);
});
it('should render the trace filter with the given query', async () => {
jest
.spyOn(compositeQueryHook, 'useGetCompositeQueryParam')
.mockReturnValue(compositeQuery);
const { findByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
// check if the default query is applied - composite query has filters - serviceName : demo-app and name : HTTP GET /customer
expect(await findByText('demo-app')).toBeInTheDocument();
expect(getByTestId('serviceName-demo-app')).toBeChecked();
expect(await findByText('HTTP GET /customer')).toBeInTheDocument();
expect(getByTestId('name-HTTP GET /customer')).toBeChecked();
});
it('test edge cases of undefined filters', async () => {
jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({
...compositeQuery,
@ -98,7 +294,6 @@ describe('TracesExplorer - ', () => {
const { getByText } = render(<Filter setOpen={jest.fn()} />);
// we should have all the filters
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument();
});
@ -124,9 +319,141 @@ describe('TracesExplorer - ', () => {
const { getByText } = render(<Filter setOpen={jest.fn()} />);
// we should have all the filters
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument();
});
});
it('should clear filter on clear & reset button click', async () => {
const { getByText, getByTestId } = render(
<QueryBuilderContext.Provider
value={
{
currentQuery: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [initialQueryBuilderFormValues],
},
},
redirectWithQueryBuilderData,
} as any
}
>
<Filter setOpen={jest.fn()} />
</QueryBuilderContext.Provider>,
);
// check for the status section content
await checkForSectionContent(['Ok', 'Error']);
// check for the service name section content from API response
await checkForSectionContent([
'customer',
'demo-app',
'driver',
'frontend',
'mysql',
'redis',
'route',
'go-grpc-otel-server',
'test',
]);
const okCheckbox = getByText('Ok');
fireEvent.click(okCheckbox);
const frontendCheckbox = getByText('frontend');
fireEvent.click(frontendCheckbox);
// check if checked and present in query
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
id: expect.any(String),
key: 'hasError',
type: 'tag',
dataType: 'bool',
isColumn: true,
isJSON: false,
},
op: 'in',
value: ['false'],
}),
expect.objectContaining({
key: {
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: expect.any(String),
},
op: 'in',
value: ['frontend'],
}),
]),
);
const clearButton = getByTestId('collapse-serviceName-clearBtn');
expect(clearButton).toBeInTheDocument();
fireEvent.click(clearButton);
// check if cleared and not present in query
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: expect.any(String),
},
op: 'in',
value: ['frontend'],
}),
]),
);
// check if reset button is present
const resetButton = getByTestId('reset-filters');
expect(resetButton).toBeInTheDocument();
fireEvent.click(resetButton);
// check if reset id done
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual([]);
});
it('filter panel should collapse & uncollapsed', async () => {
const { getByText, getByTestId } = render(<TracesExplorer />);
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument();
});
// Filter panel should collapse
const collapseButton = getByTestId('toggle-filter-panel');
expect(collapseButton).toBeInTheDocument();
fireEvent.click(collapseButton);
// uncollapse btn should be present
expect(
await screen.findByTestId('filter-uncollapse-btn'),
).toBeInTheDocument();
});
});

View File

@ -251,6 +251,7 @@ function TracesExplorer(): JSX.Element {
<Button
onClick={(): void => setOpen(!isOpen)}
className="filter-outlined-btn"
data-testid="filter-uncollapse-btn"
>
<FilterOutlined />
</Button>