mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
1180 lines
36 KiB
TypeScript
1180 lines
36 KiB
TypeScript
import {Fragment} from 'react';
|
|
import {OrganizationFixture} from 'sentry-fixture/organization';
|
|
import {TagsFixture} from 'sentry-fixture/tags';
|
|
|
|
import {
|
|
act,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
userEvent,
|
|
waitFor,
|
|
} from 'sentry-test/reactTestingLibrary';
|
|
|
|
import {SmartSearchBar} from 'sentry/components/smartSearchBar';
|
|
import TagStore from 'sentry/stores/tagStore';
|
|
import {FieldKey} from 'sentry/utils/fields';
|
|
|
|
import {ItemType} from './types';
|
|
|
|
describe('SmartSearchBar', function () {
|
|
let defaultProps;
|
|
|
|
beforeEach(function () {
|
|
TagStore.reset();
|
|
TagStore.loadTagsSuccess(TagsFixture());
|
|
const supportedTags = TagStore.getState();
|
|
supportedTags.firstRelease = {
|
|
key: 'firstRelease',
|
|
name: 'firstRelease',
|
|
};
|
|
supportedTags.is = {
|
|
key: 'is',
|
|
name: 'is',
|
|
};
|
|
|
|
const organization = OrganizationFixture({id: '123'});
|
|
|
|
const location = {
|
|
pathname: '/organizations/org-slug/recent-searches/',
|
|
query: {
|
|
projectId: '0',
|
|
},
|
|
};
|
|
|
|
MockApiClient.addMockResponse({
|
|
url: '/organizations/org-slug/recent-searches/',
|
|
body: [],
|
|
});
|
|
|
|
defaultProps = {
|
|
query: '',
|
|
organization,
|
|
location,
|
|
supportedTags,
|
|
onGetTagValues: jest.fn().mockResolvedValue([]),
|
|
onSearch: jest.fn(),
|
|
};
|
|
});
|
|
|
|
afterEach(function () {
|
|
MockApiClient.clearMockResponses();
|
|
});
|
|
|
|
it('quotes in values with spaces when autocompleting', async function () {
|
|
const onGetTagValuesMock = jest
|
|
.fn()
|
|
.mockResolvedValue(['this is filled with spaces']);
|
|
|
|
render(<SmartSearchBar {...defaultProps} onGetTagValues={onGetTagValuesMock} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.click(textbox);
|
|
await userEvent.type(textbox, 'device:this');
|
|
|
|
const option = await screen.findByText(/this is filled with spaces/);
|
|
|
|
await userEvent.click(option);
|
|
|
|
expect(textbox).toHaveValue('device:"this is filled with spaces" ');
|
|
});
|
|
|
|
it('escapes quotes in values properly when autocompleting', async function () {
|
|
const onGetTagValuesMock = jest
|
|
.fn()
|
|
.mockResolvedValue(['this " is " filled " with " quotes']);
|
|
|
|
render(<SmartSearchBar {...defaultProps} onGetTagValues={onGetTagValuesMock} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.click(textbox);
|
|
await userEvent.type(textbox, 'device:this');
|
|
|
|
const option = await screen.findByText(/this \\" is \\" filled \\" with \\" quotes/);
|
|
|
|
await userEvent.click(option);
|
|
|
|
expect(textbox).toHaveValue('device:"this \\" is \\" filled \\" with \\" quotes" ');
|
|
});
|
|
|
|
it('does not search when pressing enter on a tag without a value', async function () {
|
|
const onSearchMock = jest.fn();
|
|
|
|
render(<SmartSearchBar {...defaultProps} onSearch={onSearchMock} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'browser:{enter}');
|
|
|
|
expect(onSearchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('autocompletes value with tab', async function () {
|
|
const onSearchMock = jest.fn();
|
|
|
|
render(<SmartSearchBar {...defaultProps} onSearch={onSearchMock} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'bro');
|
|
|
|
expect(
|
|
await screen.findByRole('option', {name: 'bro wser - field'})
|
|
).toBeInTheDocument();
|
|
|
|
// down once to 'browser' dropdown item
|
|
await userEvent.keyboard('{ArrowDown}{Tab}');
|
|
|
|
await waitFor(() => {
|
|
expect(textbox).toHaveValue('browser:');
|
|
});
|
|
|
|
expect(textbox).toHaveFocus();
|
|
|
|
// Should not have executed the search
|
|
expect(onSearchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('autocompletes value with enter', async function () {
|
|
const onSearchMock = jest.fn();
|
|
|
|
render(<SmartSearchBar {...defaultProps} onSearch={onSearchMock} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'bro');
|
|
|
|
expect(
|
|
await screen.findByRole('option', {name: 'bro wser - field'})
|
|
).toBeInTheDocument();
|
|
|
|
// down once to 'browser' dropdown item
|
|
await userEvent.keyboard('{ArrowDown}{Enter}');
|
|
|
|
await waitFor(() => {
|
|
expect(textbox).toHaveValue('browser:');
|
|
});
|
|
|
|
expect(textbox).toHaveFocus();
|
|
|
|
// Should not have executed the search
|
|
expect(onSearchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('searches and completes tags with negation operator', async function () {
|
|
render(<SmartSearchBar {...defaultProps} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, '!bro');
|
|
|
|
const field = await screen.findByRole('option', {name: 'bro wser - field'});
|
|
|
|
await userEvent.click(field);
|
|
|
|
expect(textbox).toHaveValue('!browser:');
|
|
});
|
|
|
|
describe('componentWillReceiveProps()', function () {
|
|
it('should add a space when setting query', function () {
|
|
render(<SmartSearchBar {...defaultProps} query="one" />);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('one ');
|
|
});
|
|
|
|
it('updates query when prop changes', function () {
|
|
const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
|
|
|
|
rerender(<SmartSearchBar {...defaultProps} query="two" />);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('two ');
|
|
});
|
|
|
|
it('updates query when prop set to falsey value', function () {
|
|
const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
|
|
|
|
rerender(<SmartSearchBar {...defaultProps} query={null} />);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
});
|
|
|
|
it('should not reset user textarea if a noop props change happens', async function () {
|
|
const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
|
|
|
|
await userEvent.type(screen.getByRole('textbox'), 'two');
|
|
|
|
rerender(<SmartSearchBar {...defaultProps} query="one" />);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('one two');
|
|
});
|
|
|
|
it('should reset user textarea if a meaningful props change happens', async function () {
|
|
const {rerender} = render(<SmartSearchBar {...defaultProps} query="one" />);
|
|
|
|
await userEvent.type(screen.getByRole('textbox'), 'two');
|
|
|
|
rerender(<SmartSearchBar {...defaultProps} query="blah" />);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('blah ');
|
|
});
|
|
});
|
|
|
|
describe('clear search', function () {
|
|
it('clicking the clear search button clears the query and calls onSearch', async function () {
|
|
const mockOnSearch = jest.fn();
|
|
|
|
render(
|
|
<SmartSearchBar {...defaultProps} onSearch={mockOnSearch} query="is:unresolved" />
|
|
);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('is:unresolved ');
|
|
|
|
await userEvent.click(screen.getByRole('button', {name: 'Clear search'}));
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
|
|
expect(mockOnSearch).toHaveBeenCalledTimes(1);
|
|
expect(mockOnSearch).toHaveBeenCalledWith('');
|
|
});
|
|
});
|
|
|
|
describe('dropdown open state', function () {
|
|
it('opens the dropdown when the search box is clicked', async function () {
|
|
render(<SmartSearchBar {...defaultProps} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
await userEvent.click(textbox);
|
|
|
|
expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens the dropdown when the search box gains focus', function () {
|
|
render(<SmartSearchBar {...defaultProps} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
fireEvent.focus(textbox);
|
|
|
|
expect(screen.getByTestId('smart-search-dropdown')).toBeInTheDocument();
|
|
});
|
|
|
|
it('hides the drop down when clicking outside', async function () {
|
|
render(
|
|
<div data-test-id="test-container">
|
|
<SmartSearchBar {...defaultProps} />
|
|
</div>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Open the dropdown
|
|
fireEvent.focus(textbox);
|
|
|
|
await userEvent.click(screen.getByTestId('test-container'));
|
|
|
|
expect(screen.queryByTestId('smart-search-dropdown')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('hides the drop down when pressing escape', async function () {
|
|
render(<SmartSearchBar {...defaultProps} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Open the dropdown
|
|
fireEvent.focus(textbox);
|
|
|
|
await userEvent.type(textbox, '{Escape}');
|
|
|
|
expect(screen.queryByTestId('smart-search-dropdown')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('pasting', function () {
|
|
it('trims pasted content', function () {
|
|
const mockOnChange = jest.fn();
|
|
render(<SmartSearchBar {...defaultProps} onChange={mockOnChange} />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
fireEvent.paste(textbox, {clipboardData: {getData: () => ' something'}});
|
|
|
|
expect(textbox).toHaveValue('something');
|
|
expect(mockOnChange).toHaveBeenCalledWith('something', expect.anything());
|
|
});
|
|
});
|
|
|
|
it('invokes onSearch() on enter', async function () {
|
|
const mockOnSearch = jest.fn();
|
|
render(<SmartSearchBar {...defaultProps} query="test" onSearch={mockOnSearch} />);
|
|
|
|
await userEvent.type(screen.getByRole('textbox'), '{Enter}');
|
|
|
|
expect(mockOnSearch).toHaveBeenCalledWith('test');
|
|
});
|
|
|
|
it('handles an empty query', function () {
|
|
render(<SmartSearchBar {...defaultProps} query="" />);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
});
|
|
|
|
it('does not fetch tag values with environment tag and excludeEnvironment', async function () {
|
|
const getTagValuesMock = jest.fn().mockResolvedValue([]);
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
onGetTagValues={getTagValuesMock}
|
|
excludedTags={['environment']}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'environment:');
|
|
|
|
expect(getTagValuesMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not fetch tag values with timesSeen tag', async function () {
|
|
const getTagValuesMock = jest.fn().mockResolvedValue([]);
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
onGetTagValues={getTagValuesMock}
|
|
excludedTags={['environment']}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'timesSeen:');
|
|
|
|
expect(getTagValuesMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fetches and displays tag values with other tags', async function () {
|
|
const getTagValuesMock = jest.fn().mockResolvedValue([]);
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
onGetTagValues={getTagValuesMock}
|
|
excludedTags={['environment']}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'browser:');
|
|
|
|
expect(getTagValuesMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('shows correct options on cursor changes for keys and values', async function () {
|
|
const getTagValuesMock = jest.fn().mockResolvedValue([]);
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query="is:unresolved"
|
|
onGetTagValues={getTagValuesMock}
|
|
onGetRecentSearches={jest.fn().mockReturnValue([])}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole<HTMLTextAreaElement>('textbox');
|
|
|
|
// Set cursor to beginning of "is" tag
|
|
await userEvent.click(textbox);
|
|
textbox.setSelectionRange(0, 0);
|
|
|
|
// Should show "Keys" section
|
|
expect(await screen.findByText('Keys')).toBeInTheDocument();
|
|
|
|
// Set cursor to middle of "is" tag
|
|
await userEvent.keyboard('{ArrowRight}');
|
|
// Should show "Keys" and NOT "Operator Helpers" or "Values"
|
|
expect(await screen.findByText('Keys')).toBeInTheDocument();
|
|
expect(screen.queryByText('Operator Helpers')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Values')).not.toBeInTheDocument();
|
|
|
|
// Set cursor to end of "is" tag
|
|
await userEvent.keyboard('{ArrowRight}');
|
|
// Should show "Tags" and "Operator Helpers" but NOT "Values"
|
|
expect(await screen.findByText('Keys')).toBeInTheDocument();
|
|
expect(screen.queryByText('Operator Helpers')).toBeInTheDocument();
|
|
expect(screen.queryByText('Values')).not.toBeInTheDocument();
|
|
|
|
// Set cursor after the ":"
|
|
await userEvent.keyboard('{ArrowRight}');
|
|
// Should show "Values" and "Operator Helpers" but NOT "Keys"
|
|
expect(await screen.findByText('Values')).toBeInTheDocument();
|
|
expect(await screen.findByText('Operator Helpers')).toBeInTheDocument();
|
|
expect(screen.queryByText('Keys')).not.toBeInTheDocument();
|
|
|
|
// Set cursor inside value
|
|
await userEvent.keyboard('{ArrowRight}');
|
|
// Should show "Values" and NOT "Operator Helpers" or "Keys"
|
|
expect(await screen.findByText('Values')).toBeInTheDocument();
|
|
expect(screen.queryByText('Operator Helpers')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Keys')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows syntax error for incorrect tokens', function () {
|
|
render(<SmartSearchBar {...defaultProps} query="tag: is: has:" />);
|
|
|
|
// Should have three invalid tokens (tag:, is:, and has:)
|
|
expect(screen.getAllByTestId('filter-token-invalid')).toHaveLength(3);
|
|
});
|
|
|
|
it('renders nested keys correctly', async function () {
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query=""
|
|
supportedTags={{
|
|
nested: {
|
|
key: 'nested',
|
|
name: 'nested',
|
|
},
|
|
'nested.child': {
|
|
key: 'nested.child',
|
|
name: 'nested.child',
|
|
},
|
|
'nestednoparent.child': {
|
|
key: 'nestednoparent.child',
|
|
name: 'nestednoparent.child',
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'nest');
|
|
|
|
await screen.findByText('Keys');
|
|
});
|
|
|
|
it('filters keys on name and description', async function () {
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query=""
|
|
supportedTags={{
|
|
[FieldKey.DEVICE_CHARGING]: {
|
|
key: FieldKey.DEVICE_CHARGING,
|
|
},
|
|
[FieldKey.EVENT_TYPE]: {
|
|
key: FieldKey.EVENT_TYPE,
|
|
},
|
|
[FieldKey.DEVICE_ARCH]: {
|
|
key: FieldKey.DEVICE_ARCH,
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'event');
|
|
|
|
await screen.findByText('Keys');
|
|
|
|
// Should show event.type (has event in key) and device.charging (has event in description)
|
|
expect(screen.getByRole('option', {name: /event . type/})).toBeInTheDocument();
|
|
expect(screen.getByRole('option', {name: /charging/})).toBeInTheDocument();
|
|
|
|
// But not device.arch (not in key or description)
|
|
expect(screen.queryByRole('option', {name: /arch/})).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('handles autocomplete race conditions when cursor position changed', async function () {
|
|
jest.useFakeTimers();
|
|
const user = userEvent.setup({delay: null});
|
|
|
|
const mockOnGetTagValues = jest.fn().mockImplementation(
|
|
() =>
|
|
new Promise(resolve => {
|
|
setTimeout(() => {
|
|
resolve(['value']);
|
|
}, 300);
|
|
})
|
|
);
|
|
|
|
render(
|
|
<SmartSearchBar {...defaultProps} onGetTagValues={mockOnGetTagValues} query="" />
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Type key and start searching values
|
|
await user.type(textbox, 'is:');
|
|
|
|
act(() => jest.advanceTimersByTime(200));
|
|
|
|
// Before values have finished searching, clear the textbox
|
|
await user.clear(textbox);
|
|
|
|
act(jest.runAllTimers);
|
|
|
|
// Should show keys, not values in dropdown
|
|
expect(await screen.findByText('Keys')).toBeInTheDocument();
|
|
expect(screen.queryByText('Values')).not.toBeInTheDocument();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('autocompletes tag values', async function () {
|
|
const mockOnChange = jest.fn();
|
|
|
|
const getTagValuesMock = jest.fn().mockResolvedValue(['Chrome', 'Firefox']);
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
onGetTagValues={getTagValuesMock}
|
|
query=""
|
|
onChange={mockOnChange}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'browser:');
|
|
|
|
const option = await screen.findByRole('option', {name: /Firefox/});
|
|
|
|
await userEvent.click(option, {delay: null});
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnChange).toHaveBeenLastCalledWith(
|
|
'browser:Firefox ',
|
|
expect.anything()
|
|
);
|
|
});
|
|
});
|
|
|
|
it('autocompletes tag values when there are other tags', async function () {
|
|
const mockOnChange = jest.fn();
|
|
|
|
const getTagValuesMock = jest.fn().mockResolvedValue(['Chrome', 'Firefox']);
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
onGetTagValues={getTagValuesMock}
|
|
excludedTags={['environment']}
|
|
query="is:unresolved browser: error.handled:true"
|
|
onChange={mockOnChange}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'is:unresolved browser'.length,
|
|
initialSelectionEnd: 'is:unresolved browser'.length,
|
|
});
|
|
|
|
const option = await screen.findByRole('option', {name: /Firefox/});
|
|
|
|
await userEvent.click(option, {delay: null});
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnChange).toHaveBeenLastCalledWith(
|
|
'is:unresolved browser:Firefox error.handled:true ',
|
|
expect.anything()
|
|
);
|
|
});
|
|
});
|
|
|
|
it('autocompletes tag values (user tag)', async function () {
|
|
jest.useFakeTimers();
|
|
const mockOnChange = jest.fn();
|
|
const getTagValuesMock = jest.fn().mockResolvedValue(['id:1']);
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
onGetTagValues={getTagValuesMock}
|
|
query=""
|
|
onChange={mockOnChange}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'user:', {delay: null});
|
|
|
|
act(jest.runOnlyPendingTimers);
|
|
|
|
const option = await screen.findByRole('option', {name: /id:1/});
|
|
|
|
await userEvent.click(option, {delay: null});
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnChange).toHaveBeenLastCalledWith('user:"id:1" ', expect.anything());
|
|
});
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('autocompletes assigned from string values', async function () {
|
|
const mockOnChange = jest.fn();
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query=""
|
|
onChange={mockOnChange}
|
|
supportedTags={{
|
|
assigned: {
|
|
key: 'assigned',
|
|
name: 'assigned',
|
|
predefined: true,
|
|
values: ['me', '[me, none]', '#team-a'],
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'assigned:', {delay: null});
|
|
|
|
await userEvent.click(await screen.findByRole('option', {name: /#team-a/}), {
|
|
delay: null,
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnChange).toHaveBeenLastCalledWith(
|
|
'assigned:#team-a ',
|
|
expect.anything()
|
|
);
|
|
});
|
|
});
|
|
|
|
it('autocompletes assigned from SearchGroup objects', async function () {
|
|
const mockOnChange = jest.fn();
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query=""
|
|
onChange={mockOnChange}
|
|
supportedTags={{
|
|
assigned: {
|
|
key: 'assigned',
|
|
name: 'assigned',
|
|
predefined: true,
|
|
values: [
|
|
{
|
|
title: 'Suggested Values',
|
|
type: 'header',
|
|
icon: <Fragment />,
|
|
children: [
|
|
{
|
|
value: 'me',
|
|
desc: 'me',
|
|
type: ItemType.TAG_VALUE,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: 'All Values',
|
|
type: 'header',
|
|
icon: <Fragment />,
|
|
children: [
|
|
{
|
|
value: '#team-a',
|
|
desc: '#team-a',
|
|
type: ItemType.TAG_VALUE,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'assigned:', {delay: null});
|
|
|
|
expect(await screen.findByText('Suggested Values')).toBeInTheDocument();
|
|
expect(screen.getByText('All Values')).toBeInTheDocument();
|
|
|
|
// Filter down to "team"
|
|
await userEvent.type(textbox, 'team', {delay: null});
|
|
|
|
expect(screen.queryByText('Suggested Values')).not.toBeInTheDocument();
|
|
|
|
await userEvent.click(screen.getByRole('option', {name: /#team-a/}), {delay: null});
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnChange).toHaveBeenLastCalledWith(
|
|
'assigned:#team-a ',
|
|
expect.anything()
|
|
);
|
|
});
|
|
});
|
|
|
|
it('autocompletes tag values (predefined values with spaces)', async function () {
|
|
jest.useFakeTimers();
|
|
const mockOnChange = jest.fn();
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query=""
|
|
onChange={mockOnChange}
|
|
supportedTags={{
|
|
predefined: {
|
|
key: 'predefined',
|
|
name: 'predefined',
|
|
predefined: true,
|
|
values: ['predefined tag with spaces'],
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'predefined:', {delay: null});
|
|
|
|
act(jest.runOnlyPendingTimers);
|
|
|
|
const option = await screen.findByRole('option', {
|
|
name: /predefined tag with spaces/,
|
|
});
|
|
|
|
await userEvent.click(option, {delay: null});
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnChange).toHaveBeenLastCalledWith(
|
|
'predefined:"predefined tag with spaces" ',
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('autocompletes tag values (predefined values with quotes)', async function () {
|
|
jest.useFakeTimers();
|
|
const mockOnChange = jest.fn();
|
|
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query=""
|
|
onChange={mockOnChange}
|
|
supportedTags={{
|
|
predefined: {
|
|
key: 'predefined',
|
|
name: 'predefined',
|
|
predefined: true,
|
|
values: ['"predefined" "tag" "with" "quotes"'],
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.type(textbox, 'predefined:', {delay: null});
|
|
|
|
act(jest.runOnlyPendingTimers);
|
|
|
|
const option = await screen.findByRole('option', {
|
|
name: /quotes/,
|
|
});
|
|
|
|
await userEvent.click(option, {delay: null});
|
|
|
|
await waitFor(() => {
|
|
expect(mockOnChange).toHaveBeenLastCalledWith(
|
|
'predefined:"\\"predefined\\" \\"tag\\" \\"with\\" \\"quotes\\"" ',
|
|
expect.anything()
|
|
);
|
|
});
|
|
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
describe('quick actions', function () {
|
|
it('can delete tokens', async function () {
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query="is:unresolved sdk.name:sentry-cocoa has:key"
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Put cursor inside is:resolved
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 0,
|
|
initialSelectionEnd: 0,
|
|
});
|
|
|
|
await userEvent.click(screen.getByRole('button', {name: /Delete/}));
|
|
|
|
expect(textbox).toHaveValue('sdk.name:sentry-cocoa has:key');
|
|
});
|
|
|
|
it('can delete a middle token', async function () {
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query="is:unresolved sdk.name:sentry-cocoa has:key"
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Put cursor inside sdk.name
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'is:unresolved '.length,
|
|
initialSelectionEnd: 'is:unresolved '.length,
|
|
});
|
|
|
|
await userEvent.click(screen.getByRole('button', {name: /Delete/}));
|
|
|
|
expect(textbox).toHaveValue('is:unresolved has:key');
|
|
});
|
|
|
|
it('can exclude a token', async function () {
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query="is:unresolved sdk.name:sentry-cocoa has:key"
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Put cursor inside sdk.name
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'is:unresolved '.length,
|
|
initialSelectionEnd: 'is:unresolved '.length,
|
|
});
|
|
|
|
await userEvent.click(screen.getByRole('button', {name: /Exclude/}));
|
|
|
|
expect(textbox).toHaveValue('is:unresolved !sdk.name:sentry-cocoa has:key ');
|
|
});
|
|
|
|
it('can include a token', async function () {
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
query="is:unresolved !sdk.name:sentry-cocoa has:key"
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Put cursor inside sdk.name
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'is:unresolved !'.length,
|
|
initialSelectionEnd: 'is:unresolved !'.length,
|
|
});
|
|
|
|
expect(textbox).toHaveValue('is:unresolved !sdk.name:sentry-cocoa has:key ');
|
|
|
|
await screen.findByRole('button', {name: /Include/});
|
|
await userEvent.click(screen.getByRole('button', {name: /Include/}));
|
|
|
|
expect(textbox).toHaveValue('is:unresolved sdk.name:sentry-cocoa has:key ');
|
|
});
|
|
});
|
|
|
|
it('displays invalid field message', async function () {
|
|
render(<SmartSearchBar {...defaultProps} query="" />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
await userEvent.type(textbox, 'invalid:');
|
|
|
|
expect(
|
|
await screen.findByRole('option', {name: /the field invalid isn't supported here/i})
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays invalid field messages for when wildcard is disallowed', async function () {
|
|
render(<SmartSearchBar {...defaultProps} query="" disallowWildcard />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Value
|
|
await userEvent.type(textbox, 'release:*');
|
|
expect(
|
|
await screen.findByRole('option', {name: /Wildcards aren't supported here/i})
|
|
).toBeInTheDocument();
|
|
await userEvent.clear(textbox);
|
|
|
|
// FreeText
|
|
await userEvent.type(textbox, 'rel*ease');
|
|
expect(
|
|
await screen.findByRole('option', {name: /Wildcards aren't supported here/i})
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
describe('date fields', () => {
|
|
// Transpile the lazy-loaded datepicker up front so tests don't flake
|
|
beforeAll(async function () {
|
|
await import('sentry/components/calendar/datePicker');
|
|
});
|
|
|
|
it('displays date picker dropdown when appropriate', async () => {
|
|
render(<SmartSearchBar {...defaultProps} query="" />);
|
|
|
|
const textbox = screen.getByRole<HTMLTextAreaElement>('textbox');
|
|
await userEvent.click(textbox);
|
|
expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
|
|
|
|
// Just lastSeen: will display relative and absolute options, not the datepicker
|
|
await userEvent.type(textbox, 'lastSeen:');
|
|
expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
|
|
expect(screen.getByText('Last hour')).toBeInTheDocument();
|
|
expect(screen.getByText('After a custom datetime')).toBeInTheDocument();
|
|
|
|
// lastSeen:> should open the date picker
|
|
await userEvent.type(textbox, '>');
|
|
expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
|
|
|
|
// Continues to display with date typed out
|
|
await userEvent.type(textbox, '2022-01-01');
|
|
expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
|
|
|
|
// Goes away when on next term
|
|
await userEvent.type(textbox, ' ');
|
|
expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
|
|
|
|
// Pops back up when cursor is back in date token
|
|
await userEvent.keyboard('{arrowleft}');
|
|
expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
|
|
|
|
// Moving cursor inside the `lastSeen` token hides the date picker
|
|
textbox.setSelectionRange(1, 1);
|
|
await userEvent.click(textbox);
|
|
expect(screen.queryByTestId('search-bar-date-picker')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('can select a suggested relative time value', async () => {
|
|
render(<SmartSearchBar {...defaultProps} query="" />);
|
|
|
|
await userEvent.type(screen.getByRole('textbox'), 'lastSeen:');
|
|
|
|
await userEvent.click(screen.getByText('Last hour'));
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue('lastSeen:-1h ');
|
|
});
|
|
|
|
it('can select a specific date/time', async () => {
|
|
render(<SmartSearchBar {...defaultProps} query="" />);
|
|
|
|
await userEvent.type(screen.getByRole('textbox'), 'lastSeen:');
|
|
|
|
await userEvent.click(screen.getByText('After a custom datetime'));
|
|
|
|
// Should have added '>' to query and show a date picker
|
|
expect(screen.getByRole('textbox')).toHaveValue('lastSeen:>');
|
|
expect(screen.getByTestId('search-bar-date-picker')).toBeInTheDocument();
|
|
|
|
// Select a day on the calendar
|
|
const dateInput = await screen.findByTestId('date-picker');
|
|
fireEvent.change(dateInput, {target: {value: '2022-01-02'}});
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue(
|
|
// -05:00 because our tests run in EST
|
|
'lastSeen:>2022-01-02T00:00:00-05:00'
|
|
);
|
|
|
|
const timeInput = screen.getByLabelText('Time');
|
|
|
|
// Simulate changing time input one bit at a time
|
|
await userEvent.click(timeInput);
|
|
fireEvent.change(timeInput, {target: {value: '01:00:00'}});
|
|
fireEvent.change(timeInput, {target: {value: '01:02:00'}});
|
|
fireEvent.change(timeInput, {target: {value: '01:02:03'}});
|
|
// Time input should have retained focus this whole time
|
|
expect(timeInput).toHaveFocus();
|
|
fireEvent.blur(timeInput);
|
|
|
|
expect(screen.getByRole('textbox')).toHaveValue(
|
|
'lastSeen:>2022-01-02T01:02:03-05:00'
|
|
);
|
|
|
|
// Toggle UTC on, which should remove the timezone (-05:00) from the query
|
|
await userEvent.click(screen.getByLabelText('Use UTC'));
|
|
expect(screen.getByRole('textbox')).toHaveValue('lastSeen:>2022-01-02T01:02:03');
|
|
});
|
|
|
|
it('can change an existing datetime', async () => {
|
|
render(<SmartSearchBar {...defaultProps} query="" />);
|
|
|
|
const textbox = screen.getByRole<HTMLTextAreaElement>('textbox');
|
|
fireEvent.change(textbox, {
|
|
target: {value: 'lastSeen:2022-01-02 firstSeen:2022-01-01'},
|
|
});
|
|
|
|
// Move cursor to the lastSeen date
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'lastSeen:2022-01-0'.length,
|
|
initialSelectionEnd: 'lastSeen:2022-01-0'.length,
|
|
});
|
|
|
|
const dateInput = await screen.findByTestId('date-picker');
|
|
|
|
expect(dateInput).toHaveValue('2022-01-02');
|
|
expect(screen.getByLabelText('Time')).toHaveValue('00:00:00');
|
|
expect(screen.getByLabelText('Use UTC')).toBeChecked();
|
|
|
|
fireEvent.change(dateInput, {target: {value: '2022-01-03'}});
|
|
|
|
expect(textbox).toHaveValue('lastSeen:2022-01-03T00:00:00 firstSeen:2022-01-01');
|
|
|
|
// Cursor should be at end of the value we just replaced
|
|
expect(textbox.selectionStart).toBe('lastSeen:2022-01-03T00:00:00'.length);
|
|
});
|
|
|
|
it('populates the date picker correctly for date without time', async () => {
|
|
render(<SmartSearchBar {...defaultProps} query="lastSeen:2022-01-01" />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Move cursor to the timestamp
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'lastSeen:2022-01-0'.length,
|
|
initialSelectionEnd: 'lastSeen:2022-01-0'.length,
|
|
});
|
|
|
|
const dateInput = await screen.findByTestId('date-picker');
|
|
|
|
expect(dateInput).toHaveValue('2022-01-01');
|
|
// No time provided, so time input should be the default value
|
|
expect(screen.getByLabelText('Time')).toHaveValue('00:00:00');
|
|
// UTC is checked because there is no timezone
|
|
expect(screen.getByLabelText('Use UTC')).toBeChecked();
|
|
});
|
|
|
|
it('populates the date picker correctly for date with time and no timezone', async () => {
|
|
render(<SmartSearchBar {...defaultProps} query="lastSeen:2022-01-01T09:45:12" />);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Move cursor to the timestamp
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'lastSeen:2022-01-0'.length,
|
|
initialSelectionEnd: 'lastSeen:2022-01-0'.length,
|
|
});
|
|
|
|
const dateInput = await screen.findByTestId('date-picker');
|
|
|
|
expect(dateInput).toHaveValue('2022-01-01');
|
|
expect(screen.getByLabelText('Time')).toHaveValue('09:45:12');
|
|
expect(screen.getByLabelText('Use UTC')).toBeChecked();
|
|
});
|
|
|
|
it('populates the date picker correctly for date with time and timezone', async () => {
|
|
render(
|
|
<SmartSearchBar {...defaultProps} query="lastSeen:2022-01-01T09:45:12-05:00" />
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
|
|
// Move cursor to the timestamp
|
|
await userEvent.type(textbox, '{ArrowRight}', {
|
|
initialSelectionStart: 'lastSeen:2022-01-0'.length,
|
|
initialSelectionEnd: 'lastSeen:2022-01-0'.length,
|
|
});
|
|
|
|
const dateInput = await screen.findByTestId('date-picker');
|
|
|
|
expect(dateInput).toHaveValue('2022-01-01');
|
|
expect(screen.getByLabelText('Time')).toHaveValue('09:45:12');
|
|
expect(screen.getByLabelText('Use UTC')).not.toBeChecked();
|
|
});
|
|
});
|
|
|
|
describe('defaultSearchGroup', () => {
|
|
const defaultSearchGroup = {
|
|
title: 'default search group',
|
|
type: 'header',
|
|
// childrenWrapper allows us to arrange the children with custom styles
|
|
childrenWrapper: props => (
|
|
<div data-test-id="default-search-group-wrapper" {...props} />
|
|
),
|
|
children: [
|
|
{
|
|
type: ItemType.RECOMMENDED,
|
|
title: 'Assignee',
|
|
value: 'assigned_or_suggested:',
|
|
},
|
|
],
|
|
};
|
|
|
|
it('displays a default group with custom wrapper', async function () {
|
|
const mockOnChange = jest.fn();
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
defaultSearchGroup={defaultSearchGroup}
|
|
query=""
|
|
onChange={mockOnChange}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.click(textbox);
|
|
|
|
expect(screen.getByTestId('default-search-group-wrapper')).toBeInTheDocument();
|
|
expect(screen.getByText('default search group')).toBeInTheDocument();
|
|
|
|
// Default group is correctly added to the dropdown
|
|
await userEvent.keyboard('{ArrowDown}{Enter}');
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(
|
|
'assigned_or_suggested:',
|
|
expect.anything()
|
|
);
|
|
});
|
|
it('hides the default group after typing', async function () {
|
|
render(
|
|
<SmartSearchBar {...defaultProps} defaultSearchGroup={defaultSearchGroup} />
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.click(textbox);
|
|
|
|
expect(screen.getByTestId('default-search-group-wrapper')).toBeInTheDocument();
|
|
|
|
await userEvent.type(textbox, 'f');
|
|
|
|
expect(
|
|
screen.queryByTestId('default-search-group-wrapper')
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('hides the default group after picking item with applyFilter', async function () {
|
|
render(
|
|
<SmartSearchBar
|
|
{...defaultProps}
|
|
defaultSearchGroup={{
|
|
...defaultSearchGroup,
|
|
children: [
|
|
{
|
|
type: ItemType.RECOMMENDED,
|
|
title: 'Custom Tags',
|
|
// Filter is applied to all search items when picked
|
|
applyFilter: item => item.title === 'device',
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const textbox = screen.getByRole('textbox');
|
|
await userEvent.click(textbox);
|
|
expect(await screen.findByText('User identification value')).toBeInTheDocument();
|
|
await userEvent.click(screen.getByText('Custom Tags'));
|
|
|
|
expect(screen.queryByText('Custom Tags')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('User identification value')).not.toBeInTheDocument();
|
|
expect(screen.getByText('device')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|