319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
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('select-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}
|
|
/>
|
|
);
|
|
};
|