The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx

318 lines
11 KiB

import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LanguageProvider } from '@grafana/data';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource';
import TempoLanguageProvider from '../language_provider';
import { initTemplateSrv } from '../test/test_utils';
import { keywordOperators, numberOperators, operators, stringOperators } from '../traceql/traceql';
import SearchField from './SearchField';
describe('SearchField', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
const expectedValues = {
interpolationVar: 'interpolationText',
interpolationText: 'interpolationText',
interpolationVarWithPipe: 'interpolationTextOne|interpolationTextTwo',
scopedInterpolationText: 'scopedInterpolationText',
};
initTemplateSrv([{ name: 'templateVariable1' }, { name: 'templateVariable2' }], expectedValues);
jest.useFakeTimers();
// Need to use delay: null here to work with fakeTimers
// see https://github.com/testing-library/user-event/issues/833
user = userEvent.setup({ delay: null });
});
afterEach(() => {
jest.useRealTimers();
});
it('should not render tag if hideTag is true', async () => {
const updateFilter = jest.fn((val) => {
return val;
});
const filter: TraceqlFilter = { id: 'test1', valueType: 'string', tag: 'test-tag' };
const { container } = renderSearchField(updateFilter, filter, [], true);
await waitFor(async () => {
expect(container.querySelector(`input[aria-label="select test1 tag"]`)).not.toBeInTheDocument();
expect(container.querySelector(`input[aria-label="select test1 operator"]`)).toBeInTheDocument();
expect(container.querySelector(`input[aria-label="select test1 value"]`)).toBeInTheDocument();
});
});
it('should update operator when new value is selected in operator input', async () => {
const updateFilter = jest.fn((val) => {
return val;
});
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const { container } = renderSearchField(updateFilter, filter);
const select = container.querySelector(`input[aria-label="select test1 operator"]`);
expect(select).not.toBeNull();
expect(select).toBeInTheDocument();
if (select) {
await user.click(select);
jest.advanceTimersByTime(1000);
const largerThanOp = await screen.findByText('!=');
await user.click(largerThanOp);
expect(updateFilter).toHaveBeenCalledWith({ ...filter, operator: '!=' });
}
});
it('should update value when new value is selected in value input', async () => {
const updateFilter = jest.fn((val) => {
return val;
});
const filter: TraceqlFilter = {
id: 'test1',
valueType: 'string',
tag: 'test-tag',
};
const { container } = renderSearchField(updateFilter, filter);
const select = container.querySelector(`input[aria-label="select test1 value"]`);
expect(select).not.toBeNull();
expect(select).toBeInTheDocument();
if (select) {
// Add first value
await user.click(select);
await act(async () => {
jest.advanceTimersByTime(1000);
});
const driverVal = await screen.findByText('driver');
await act(async () => {
await user.click(driverVal);
});
expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: ['driver'] });
// Add a second value
await user.click(select);
await act(async () => {
jest.advanceTimersByTime(1000);
});
const customerVal = await screen.findByText('customer');
await user.click(customerVal);
expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: ['driver', 'customer'] });
// Remove the first value
const firstValRemove = await screen.findAllByLabelText('Remove');
await user.click(firstValRemove[0]);
expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: ['customer'] });
}
});
it('should update tag when new value is selected in tag input', async () => {
const updateFilter = jest.fn((val) => {
return val;
});
const filter: TraceqlFilter = {
id: 'test1',
valueType: 'string',
};
const { container } = renderSearchField(updateFilter, filter, ['tag1', 'tag22', 'tag33']);
const select = container.querySelector(`input[aria-label="select test1 tag"]`);
expect(select).not.toBeNull();
expect(select).toBeInTheDocument();
if (select) {
// Select tag22 as the tag
await user.click(select);
await act(async () => {
jest.advanceTimersByTime(1000);
});
const tag22 = await screen.findByText('tag22');
await user.click(tag22);
expect(updateFilter).toHaveBeenCalledWith({ ...filter, tag: 'tag22', value: [] });
// Select tag1 as the tag
await user.click(select);
await act(async () => {
jest.advanceTimersByTime(1000);
});
const tag1 = await screen.findByText('tag1');
await user.click(tag1);
expect(updateFilter).toHaveBeenCalledWith({ ...filter, tag: 'tag1', value: [] });
// Remove the tag
const tagRemove = await screen.findByLabelText('Clear value');
await user.click(tagRemove);
expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: [] });
}
});
it('should provide intrinsic as a selectable scope', async () => {
const updateFilter = jest.fn((val) => {
return val;
});
const filter: TraceqlFilter = { id: 'test1', valueType: 'string', tag: 'test-tag' };
const { container } = renderSearchField(updateFilter, filter, [], true);
const scopeSelect = container.querySelector(`input[aria-label="select test1 scope"]`);
expect(scopeSelect).not.toBeNull();
expect(scopeSelect).toBeInTheDocument();
if (scopeSelect) {
await user.click(scopeSelect);
jest.advanceTimersByTime(1000);
expect(await screen.findByText('resource')).toBeInTheDocument();
expect(await screen.findByText('span')).toBeInTheDocument();
expect(await screen.findByText('unscoped')).toBeInTheDocument();
expect(await screen.findByText('intrinsic')).toBeInTheDocument();
expect(await screen.findByText('$templateVariable1')).toBeInTheDocument();
expect(await screen.findByText('$templateVariable2')).toBeInTheDocument();
}
});
it('should only show keyword operators if options tag type is keyword', async () => {
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const lp = {
getOptionsV2: jest.fn().mockReturnValue([
{
value: 'ok',
label: 'ok',
type: 'keyword',
},
]),
getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
const select = container.querySelector(`input[aria-label="select test1 operator"]`);
if (select) {
await user.click(select);
await waitFor(async () => {
expect(screen.getByText('Equals')).toBeInTheDocument();
expect(screen.getByText('Not equals')).toBeInTheDocument();
operators
.filter((op) => !keywordOperators.includes(op))
.forEach((op) => {
expect(screen.queryByText(op)).not.toBeInTheDocument();
});
});
}
});
it('should only show string operators if options tag type is string', async () => {
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const { container } = renderSearchField(jest.fn(), filter);
const select = container.querySelector(`input[aria-label="select test1 operator"]`);
if (select) {
await user.click(select);
await waitFor(async () => {
expect(screen.getByText('Equals')).toBeInTheDocument();
expect(screen.getByText('Not equals')).toBeInTheDocument();
expect(screen.getByText('Matches regex')).toBeInTheDocument();
expect(screen.getByText('Does not match regex')).toBeInTheDocument();
operators
.filter((op) => !stringOperators.includes(op))
.forEach((op) => {
expect(screen.queryByText(op)).not.toBeInTheDocument();
});
});
}
});
it('should only show number operators if options tag type is number', async () => {
const filter: TraceqlFilter = { id: 'test1', operator: '=', valueType: 'string', tag: 'test-tag' };
const lp = {
getOptionsV2: jest.fn().mockReturnValue([
{
value: 200,
label: 200,
type: 'int',
},
]),
getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
const select = container.querySelector(`input[aria-label="select test1 operator"]`);
if (select) {
await user.click(select);
await waitFor(async () => {
expect(screen.getByText('Equals')).toBeInTheDocument();
expect(screen.getByText('Not equals')).toBeInTheDocument();
expect(screen.getByText('Greater')).toBeInTheDocument();
expect(screen.getByText('Less')).toBeInTheDocument();
expect(screen.getByText('Greater or Equal')).toBeInTheDocument();
expect(screen.getByText('Less or Equal')).toBeInTheDocument();
operators
.filter((op) => !numberOperators.includes(op))
.forEach((op) => {
expect(screen.queryByText(op)).not.toBeInTheDocument();
});
});
}
});
});
const renderSearchField = (
updateFilter: (f: TraceqlFilter) => void,
filter: TraceqlFilter,
tags?: string[],
hideTag?: boolean,
lp?: LanguageProvider
) => {
const languageProvider =
lp ||
({
getOptionsV2: jest.fn().mockReturnValue([
{
value: 'customer',
label: 'customer',
type: 'string',
},
{
value: 'driver',
label: 'driver',
type: 'string',
},
]),
getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider);
const datasource: TempoDatasource = {
search: {
filters: [
{
id: 'service-name',
tag: 'service.name',
operator: '=',
scope: TraceqlSearchScope.Resource,
},
{ id: 'span-name', type: 'static', tag: 'name', operator: '=', scope: TraceqlSearchScope.Span },
],
},
languageProvider,
} as TempoDatasource;
return render(
<SearchField
datasource={datasource}
updateFilter={updateFilter}
filter={filter}
setError={() => {}}
tags={tags || []}
hideTag={hideTag}
query={'{}'}
addVariablesToOptions={true}
/>
);
};