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 (
-
+
-
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 {