import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { initTemplateSrv } from 'test/helpers/initTemplateSrv'; import { LanguageProvider } from '@grafana/data'; import { FetchError, setTemplateSrv } from '@grafana/runtime'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; import { TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; import { keywordOperators, numberOperators, operators, stringOperators } from '../traceql/traceql'; import SearchField from './SearchField'; describe('SearchField', () => { let templateSrv = initTemplateSrv('key', [{ name: 'templateVariable1' }, { name: 'templateVariable2' }]); let user: ReturnType; beforeEach(() => { setTemplateSrv(templateSrv); 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); jest.advanceTimersByTime(1000); const driverVal = await screen.findByText('driver'); await user.click(driverVal); expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: ['driver'] }); // Add a second value await user.click(select); 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); jest.advanceTimersByTime(1000); const tag22 = await screen.findByText('tag22'); await user.click(tag22); expect(updateFilter).toHaveBeenCalledWith({ ...filter, tag: 'tag22' }); // Select tag1 as the tag await user.click(select); jest.advanceTimersByTime(1000); const tag1 = await screen.findByText('tag1'); await user.click(tag1); expect(updateFilter).toHaveBeenCalledWith({ ...filter, tag: 'tag1' }); // Remove the tag const tagRemove = await screen.findByLabelText('select-clear-value'); await user.click(tagRemove); expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: undefined }); } }); it('should not 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(screen.queryByText('intrinsic')).not.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', }, ]), } 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', }, ]), } 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', }, ]), } 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( ); };