12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181 |
- 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', async 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');
- await waitFor(() =>
- 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();
- });
- });
- });
|