From 89fd3e4f555dea5c64da9a56ead6aade703d0d53 Mon Sep 17 00:00:00 2001
From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
Date: Mon, 22 Jul 2024 11:05:20 +0530
Subject: [PATCH] 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
---
.../src/container/TimeSeriesView/styles.ts | 3 +-
frontend/src/container/TraceDetail/index.tsx | 5 +-
.../TracesExplorer/QuerySection/styles.ts | 3 +-
.../__mockdata__/explorer_views.ts | 81 +++++
frontend/src/mocks-server/handlers.ts | 73 ++++
.../TracesExplorer/Filter/DurationSection.tsx | 2 +
.../pages/TracesExplorer/Filter/Filter.tsx | 7 +-
.../pages/TracesExplorer/Filter/Section.tsx | 8 +-
.../TracesExplorer/Filter/SectionContent.tsx | 1 +
.../__test__/TracesExplorer.test.tsx | 333 +++++++++++++++++-
frontend/src/pages/TracesExplorer/index.tsx | 1 +
11 files changed, 505 insertions(+), 12 deletions(-)
create mode 100644 frontend/src/mocks-server/__mockdata__/explorer_views.ts
diff --git a/frontend/src/container/TimeSeriesView/styles.ts b/frontend/src/container/TimeSeriesView/styles.ts
index d73f11c38a..41a730161d 100644
--- a/frontend/src/container/TimeSeriesView/styles.ts
+++ b/frontend/src/container/TimeSeriesView/styles.ts
@@ -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)`
diff --git a/frontend/src/container/TraceDetail/index.tsx b/frontend/src/container/TraceDetail/index.tsx
index 568ed3c4f4..38f6db7c08 100644
--- a/frontend/src/container/TraceDetail/index.tsx
+++ b/frontend/src/container/TraceDetail/index.tsx
@@ -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),
diff --git a/frontend/src/container/TracesExplorer/QuerySection/styles.ts b/frontend/src/container/TracesExplorer/QuerySection/styles.ts
index cdb46bd580..a688b0dbcb 100644
--- a/frontend/src/container/TracesExplorer/QuerySection/styles.ts
+++ b/frontend/src/container/TracesExplorer/QuerySection/styles.ts
@@ -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)`
diff --git a/frontend/src/mocks-server/__mockdata__/explorer_views.ts b/frontend/src/mocks-server/__mockdata__/explorer_views.ts
new file mode 100644
index 0000000000..ae88071e55
--- /dev/null
+++ b/frontend/src/mocks-server/__mockdata__/explorer_views.ts
@@ -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"}',
+ },
+ ],
+};
diff --git a/frontend/src/mocks-server/handlers.ts b/frontend/src/mocks-server/handlers.ts
index 35ede82e83..8381818981 100644
--- a/frontend/src/mocks-server/handlers.ts
+++ b/frontend/src/mocks-server/handlers.ts
@@ -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',
+ }),
+ ),
+ ),
];
diff --git a/frontend/src/pages/TracesExplorer/Filter/DurationSection.tsx b/frontend/src/pages/TracesExplorer/Filter/DurationSection.tsx
index ce124f623e..7cf2441a49 100644
--- a/frontend/src/pages/TracesExplorer/Filter/DurationSection.tsx
+++ b/frontend/src/pages/TracesExplorer/Filter/DurationSection.tsx
@@ -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"
/>
diff --git a/frontend/src/pages/TracesExplorer/Filter/Filter.tsx b/frontend/src/pages/TracesExplorer/Filter/Filter.tsx
index 3d3895e047..2893fca2ba 100644
--- a/frontend/src/pages/TracesExplorer/Filter/Filter.tsx
+++ b/frontend/src/pages/TracesExplorer/Filter/Filter.tsx
@@ -224,13 +224,18 @@ export function Filter(props: FilterProps): JSX.Element {
-
diff --git a/frontend/src/pages/TracesExplorer/Filter/Section.tsx b/frontend/src/pages/TracesExplorer/Filter/Section.tsx
index 8ce2007ef7..9212f610b0 100644
--- a/frontend/src/pages/TracesExplorer/Filter/Section.tsx
+++ b/frontend/src/pages/TracesExplorer/Filter/Section.tsx
@@ -64,7 +64,7 @@ export function Section(props: SectionProps): JSX.Element {
return (
-
+
-
+
Clear All
diff --git a/frontend/src/pages/TracesExplorer/Filter/SectionContent.tsx b/frontend/src/pages/TracesExplorer/Filter/SectionContent.tsx
index 4cefaaeca0..2bae1dfe16 100644
--- a/frontend/src/pages/TracesExplorer/Filter/SectionContent.tsx
+++ b/frontend/src/pages/TracesExplorer/Filter/SectionContent.tsx
@@ -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}`}
>
diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx
index 1250a3f3cc..1dcaaaa4cf 100644
--- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx
+++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx
@@ -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
MockDateTimeSelection
;
+ },
+);
+
+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
{
+ 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();
+
+ 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();
+
+ // 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(
+
+
+ ,
+ );
+
+ 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();
+
+ // 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();
- // we should have all the filters
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument();
});
@@ -124,9 +319,141 @@ describe('TracesExplorer - ', () => {
const { getByText } = render();
- // 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(
+
+
+ ,
+ );
+
+ // 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();
+
+ 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();
+ });
});
diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx
index e598673c28..bb25a37f86 100644
--- a/frontend/src/pages/TracesExplorer/index.tsx
+++ b/frontend/src/pages/TracesExplorer/index.tsx
@@ -251,6 +251,7 @@ function TracesExplorer(): JSX.Element {
setOpen(!isOpen)}
className="filter-outlined-btn"
+ data-testid="filter-uncollapse-btn"
>