import {mountWithTheme} from 'sentry-test/enzyme'; import {Client} from 'sentry/api'; import {SmartSearchBar} from 'sentry/components/smartSearchBar'; import {ShortcutType} from 'sentry/components/smartSearchBar/types'; import {shortcuts} from 'sentry/components/smartSearchBar/utils'; import TagStore from 'sentry/stores/tagStore'; describe('SmartSearchBar', function () { let location, options, organization, supportedTags; let environmentTagValuesMock; const tagValuesMock = jest.fn(() => Promise.resolve([])); const mockCursorPosition = (component, pos) => { delete component.cursorPosition; Object.defineProperty(component, 'cursorPosition', { get: jest.fn().mockReturnValue(pos), configurable: true, }); }; beforeEach(function () { TagStore.reset(); TagStore.loadTagsSuccess(TestStubs.Tags()); tagValuesMock.mockClear(); supportedTags = TagStore.getAllTags(); supportedTags.firstRelease = { key: 'firstRelease', name: 'firstRelease', }; supportedTags.is = { key: 'is', name: 'is', }; organization = TestStubs.Organization({id: '123'}); location = { pathname: '/organizations/org-slug/recent-searches/', query: { projectId: '0', }, }; options = TestStubs.routerContext([ { organization, location, router: {location}, }, ]); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', body: [], }); environmentTagValuesMock = MockApiClient.addMockResponse({ url: '/projects/123/456/tags/environment/values/', body: [], }); }); afterEach(function () { MockApiClient.clearMockResponses(); }); it('quotes in values with spaces when autocompleting', async function () { jest.useRealTimers(); const getTagValuesMock = jest.fn().mockImplementation(() => { return Promise.resolve(['this is filled with spaces']); }); const onSearch = jest.fn(); const props = { orgId: 'org-slug', projectId: '0', query: '', location, organization, supportedTags, onGetTagValues: getTagValuesMock, onSearch, }; const searchBar = mountWithTheme( , options ); searchBar.find('textarea').simulate('focus'); searchBar.find('textarea').simulate('change', {target: {value: 'device:this'}}); await tick(); const preventDefault = jest.fn(); searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'}); searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault}); await tick(); expect(searchBar.find('textarea').props().value).toEqual( 'device:"this is filled with spaces" ' ); }); it('escapes quotes in values properly when autocompleting', async function () { jest.useRealTimers(); const getTagValuesMock = jest.fn().mockImplementation(() => { return Promise.resolve(['this " is " filled " with " quotes']); }); const onSearch = jest.fn(); const props = { orgId: 'org-slug', projectId: '0', query: '', location, organization, supportedTags, onGetTagValues: getTagValuesMock, onSearch, }; const searchBar = mountWithTheme( , options ); searchBar.find('textarea').simulate('focus'); searchBar.find('textarea').simulate('change', {target: {value: 'device:this'}}); await tick(); const preventDefault = jest.fn(); searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'}); searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault}); await tick(); expect(searchBar.find('textarea').props().value).toEqual( 'device:"this \\" is \\" filled \\" with \\" quotes" ' ); }); it('does not preventDefault when there are no search items and is loading and enter is pressed', async function () { jest.useRealTimers(); const getTagValuesMock = jest.fn().mockImplementation(() => { return new Promise(() => {}); }); const onSearch = jest.fn(); const props = { orgId: 'org-slug', projectId: '0', query: '', location, organization, supportedTags, onGetTagValues: getTagValuesMock, onSearch, }; const searchBar = mountWithTheme( , options ); searchBar.find('textarea').simulate('focus'); searchBar.find('textarea').simulate('change', {target: {value: 'browser:'}}); await tick(); // press enter const preventDefault = jest.fn(); searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault}); expect(onSearch).not.toHaveBeenCalled(); expect(preventDefault).not.toHaveBeenCalled(); }); it('calls preventDefault when there are existing search items and is loading and enter is pressed', async function () { jest.useRealTimers(); const getTagValuesMock = jest.fn().mockImplementation(() => { return new Promise(() => {}); }); const onSearch = jest.fn(); const props = { orgId: 'org-slug', projectId: '0', query: '', location, organization, supportedTags, onGetTagValues: getTagValuesMock, onSearch, }; const searchBar = mountWithTheme( , options ); searchBar.find('textarea').simulate('focus'); searchBar.find('textarea').simulate('change', {target: {value: 'bro'}}); await tick(); // Can't select with tab searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'}); searchBar.find('textarea').simulate('keyDown', {key: 'Tab'}); expect(onSearch).not.toHaveBeenCalled(); searchBar.find('textarea').simulate('change', {target: {value: 'browser:'}}); await tick(); // press enter const preventDefault = jest.fn(); searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault}); expect(onSearch).not.toHaveBeenCalled(); // Prevent default since we need to select an item expect(preventDefault).toHaveBeenCalled(); }); describe('componentWillReceiveProps()', function () { it('should add a space when setting state.query', function () { const searchBar = mountWithTheme( , options ); expect(searchBar.state().query).toEqual('one '); }); it('should update state.query if props.query is updated from outside', function () { const searchBar = mountWithTheme( , options ); searchBar.setProps({query: 'two'}); expect(searchBar.state().query).toEqual('two '); }); it('should update state.query if props.query is updated to null/undefined from outside', function () { const searchBar = mountWithTheme( , options ); searchBar.setProps({query: null}); expect(searchBar.state().query).toEqual(''); }); it('should not reset user textarea if a noop props change happens', function () { const searchBar = mountWithTheme( , options ); searchBar.setState({query: 'two'}); searchBar.setProps({query: 'one'}); expect(searchBar.state().query).toEqual('two'); }); it('should reset user textarea if a meaningful props change happens', function () { const searchBar = mountWithTheme( , options ); searchBar.setState({query: 'two'}); searchBar.setProps({query: 'three'}); expect(searchBar.state().query).toEqual('three '); }); }); describe('clearSearch()', function () { it('clears the query', function () { const props = { organization, location, query: 'is:unresolved ruby', defaultQuery: 'is:unresolved', supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.clearSearch(); expect(searchBar.state.query).toEqual(''); }); it('calls onSearch()', async function () { const props = { organization, location, query: 'is:unresolved ruby', defaultQuery: 'is:unresolved', supportedTags, onSearch: jest.fn(), }; const searchBar = mountWithTheme(, options).instance(); await searchBar.clearSearch(); expect(props.onSearch).toHaveBeenCalledWith(''); }); }); describe('onQueryFocus()', function () { it('displays the drop down', function () { const searchBar = mountWithTheme( , options ).instance(); expect(searchBar.state.inputHasFocus).toBe(false); searchBar.onQueryFocus(); expect(searchBar.state.inputHasFocus).toBe(true); }); it('displays dropdown in hasPinnedSearch mode', function () { const searchBar = mountWithTheme( , options ).instance(); expect(searchBar.state.inputHasFocus).toBe(false); searchBar.onQueryFocus(); expect(searchBar.state.inputHasFocus).toBe(true); }); }); describe('onQueryBlur()', function () { it('hides the drop down', function () { const searchBar = mountWithTheme( , options ).instance(); searchBar.state.inputHasFocus = true; jest.useFakeTimers(); searchBar.onQueryBlur({target: {value: 'test'}}); jest.advanceTimersByTime(201); // doesn't close until 200ms expect(searchBar.state.inputHasFocus).toBe(false); }); }); describe('onPaste()', function () { it('trims pasted content', function () { const onChange = jest.fn(); const wrapper = mountWithTheme( , options ); wrapper.setState({inputHasFocus: true}); const input = ' something '; wrapper .find('textarea') .simulate('paste', {clipboardData: {getData: () => input, value: input}}); wrapper.update(); expect(onChange).toHaveBeenCalledWith('something', expect.anything()); }); }); describe('onKeyUp()', function () { describe('escape', function () { it('blurs the textarea', function () { const wrapper = mountWithTheme( , options ); wrapper.setState({inputHasFocus: true}); const instance = wrapper.instance(); jest.spyOn(instance, 'blur'); wrapper.find('textarea').simulate('keyup', {key: 'Escape'}); expect(instance.blur).toHaveBeenCalledTimes(1); }); }); }); describe('render()', function () { it('invokes onSearch() when submitting the form', function () { const stubbedOnSearch = jest.fn(); const wrapper = mountWithTheme( , options ); wrapper.find('form').simulate('submit', { preventDefault() {}, }); expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved'); }); it('invokes onSearch() when search is cleared', async function () { jest.useRealTimers(); const props = { organization, location, query: 'is:unresolved', supportedTags, onSearch: jest.fn(), }; const wrapper = mountWithTheme(, options); wrapper.find('button[aria-label="Clear search"]').simulate('click'); await tick(); expect(props.onSearch).toHaveBeenCalledWith(''); }); it('invokes onSearch() on submit in hasPinnedSearch mode', function () { const stubbedOnSearch = jest.fn(); const wrapper = mountWithTheme( , options ); wrapper.find('form').simulate('submit'); expect(stubbedOnSearch).toHaveBeenCalledWith('is:unresolved'); }); }); it('handles an empty query', function () { const props = { query: '', defaultQuery: 'is:unresolved', organization, location, supportedTags, }; const wrapper = mountWithTheme(, options); expect(wrapper.state('query')).toEqual(''); }); describe('updateAutoCompleteItems()', function () { beforeEach(function () { jest.useFakeTimers(); }); it('sets state when empty', function () { const props = { query: '', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.updateAutoCompleteItems(); expect(searchBar.state.searchTerm).toEqual(''); expect(searchBar.state.searchGroups).toEqual([]); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('sets state when incomplete tag', async function () { const props = { query: 'fu', organization, location, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); wrapper.find('textarea').simulate('focus'); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); expect(searchBar.state.searchTerm).toEqual('fu'); expect(searchBar.state.searchGroups).toEqual([ expect.objectContaining({children: []}), ]); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('sets state when incomplete tag has negation operator', async function () { const props = { query: '!fu', organization, location, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); wrapper.find('textarea').simulate('focus'); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); expect(searchBar.state.searchTerm).toEqual('fu'); expect(searchBar.state.searchGroups).toEqual([ expect.objectContaining({children: []}), ]); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('sets state when incomplete tag as second textarea', async function () { const props = { query: 'is:unresolved fu', organization, location, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); // Cursor is at end of line mockCursorPosition(searchBar, 15); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); expect(searchBar.state.searchTerm).toEqual('fu'); // 2 items because of headers ("Tags") expect(searchBar.state.searchGroups).toHaveLength(1); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('does not request values when tag is environments', function () { const props = { query: 'environment:production', excludeEnvironment: true, location, organization, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.updateAutoCompleteItems(); jest.advanceTimersByTime(301); expect(environmentTagValuesMock).not.toHaveBeenCalled(); }); it('does not request values when tag is `timesSeen`', function () { // This should never get called const mock = MockApiClient.addMockResponse({ url: '/projects/123/456/tags/timesSeen/values/', body: [], }); const props = { query: 'timesSeen:', organization, supportedTags, }; const searchBar = mountWithTheme( , options ).instance(); searchBar.updateAutoCompleteItems(); jest.advanceTimersByTime(301); expect(mock).not.toHaveBeenCalled(); }); it('requests values when tag is `firstRelease`', function () { const mock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/releases/', body: [], }); const props = { orgId: 'org-slug', projectId: '0', query: 'firstRelease:', location, organization, supportedTags, }; const searchBar = mountWithTheme( , options ).instance(); mockCursorPosition(searchBar, 13); searchBar.updateAutoCompleteItems(); jest.advanceTimersByTime(301); expect(mock).toHaveBeenCalledWith( '/organizations/org-slug/releases/', expect.objectContaining({ method: 'GET', query: { project: '0', per_page: 5, // Limit results to 5 for autocomplete }, }) ); }); it('shows operator autocompletion', async function () { const props = { query: 'is:unresolved', organization, location, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); // Cursor is on ':' mockCursorPosition(searchBar, 3); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); // two search groups because of operator suggestions expect(searchBar.state.searchGroups).toHaveLength(2); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('responds to cursor changes', async function () { const props = { query: 'is:unresolved', organization, location, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); // Cursor is on ':' mockCursorPosition(searchBar, 3); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); // two search groups tags and values expect(searchBar.state.searchGroups).toHaveLength(2); expect(searchBar.state.activeSearchItem).toEqual(-1); mockCursorPosition(searchBar, 1); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); // one search group because showing tags expect(searchBar.state.searchGroups).toHaveLength(1); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('shows errors on incorrect tokens', function () { const props = { query: 'tag: is: has: ', organization, location, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); wrapper.find('Filter').forEach(filter => { expect(filter.prop('invalid')).toBe(true); }); }); it('handles autocomplete race conditions when cursor position changed', async function () { const props = { query: 'is:', organization, location, supportedTags, }; jest.useFakeTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); // Cursor is on ':' searchBar.generateValueAutocompleteGroup = jest.fn( () => new Promise(resolve => { setTimeout(() => { resolve({ searchItems: [], recentSearchItems: [], tagName: 'test', type: 'value', }); }, [300]); }) ); mockCursorPosition(searchBar, 3); searchBar.updateAutoCompleteItems(); jest.advanceTimersByTime(200); // Move cursor off of the place the update was called before it's done at 300ms mockCursorPosition(searchBar, 0); jest.advanceTimersByTime(101); // Get the pending promises to resolve await Promise.resolve(); wrapper.update(); expect(searchBar.state.searchGroups).toHaveLength(0); }); it('handles race conditions when query changes from default state', async function () { const props = { query: '', organization, location, supportedTags, }; jest.useFakeTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); // Cursor is on ':' searchBar.getRecentSearches = jest.fn( () => new Promise(resolve => { setTimeout(() => { resolve([]); }, [300]); }) ); mockCursorPosition(searchBar, 0); searchBar.updateAutoCompleteItems(); jest.advanceTimersByTime(200); // Change query before it's done at 300ms searchBar.updateQuery('is:'); jest.advanceTimersByTime(101); // Get the pending promises to resolve await Promise.resolve(); wrapper.update(); expect(searchBar.state.searchGroups).toHaveLength(0); }); it('correctly groups nested keys', async function () { const props = { query: 'nest', organization, location, supportedTags: { nested: { key: 'nested', name: 'nested', }, 'nested.child': { key: 'nested.child', name: 'nested.child', }, }, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); // Cursor is at end of line mockCursorPosition(searchBar, 4); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); expect(searchBar.state.searchGroups).toHaveLength(1); expect(searchBar.state.searchGroups[0].children).toHaveLength(1); expect(searchBar.state.searchGroups[0].children[0].title).toBe('nested'); expect(searchBar.state.searchGroups[0].children[0].children).toHaveLength(1); expect(searchBar.state.searchGroups[0].children[0].children[0].title).toBe( 'nested.child' ); }); it('correctly groups nested keys without a parent', async function () { const props = { query: 'nest', organization, location, supportedTags: { 'nested.child1': { key: 'nested.child1', name: 'nested.child1', }, 'nested.child2': { key: 'nested.child2', name: 'nested.child2', }, }, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); // Cursor is at end of line mockCursorPosition(searchBar, 4); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); expect(searchBar.state.searchGroups).toHaveLength(1); expect(searchBar.state.searchGroups[0].children).toHaveLength(1); expect(searchBar.state.searchGroups[0].children[0].title).toBe('nested'); expect(searchBar.state.searchGroups[0].children[0].children).toHaveLength(2); expect(searchBar.state.searchGroups[0].children[0].children[0].title).toBe( 'nested.child1' ); expect(searchBar.state.searchGroups[0].children[0].children[1].title).toBe( 'nested.child2' ); }); }); describe('onAutoComplete()', function () { it('completes terms from the list', function () { const props = { query: 'event.type:error ', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.onAutoComplete('myTag:', {type: 'tag'}); expect(searchBar.state.query).toEqual('event.type:error myTag:'); }); it('completes values if cursor is not at the end', function () { const props = { query: 'id: event.type:error ', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); mockCursorPosition(searchBar, 3); searchBar.onAutoComplete('12345', {type: 'tag-value'}); expect(searchBar.state.query).toEqual('id:12345 event.type:error '); }); it('completes values if cursor is at the end', function () { const props = { query: 'event.type:error id:', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); mockCursorPosition(searchBar, 20); searchBar.onAutoComplete('12345', {type: 'tag-value'}); expect(searchBar.state.query).toEqual('event.type:error id:12345 '); }); it('triggers onChange', function () { const onChange = jest.fn(); const props = { query: 'event.type:error id:', organization, location, supportedTags, }; const searchBar = mountWithTheme( , options ).instance(); mockCursorPosition(searchBar, 20); searchBar.onAutoComplete('12345', {type: 'tag-value'}); expect(onChange).toHaveBeenCalledWith( 'event.type:error id:12345 ', expect.anything() ); }); it('keeps the negation operator is present', function () { const props = { query: '', organization, location, supportedTags, }; const smartSearchBar = mountWithTheme(, options); const searchBar = smartSearchBar.instance(); const textarea = smartSearchBar.find('textarea'); // start typing part of the tag prefixed by the negation operator! textarea.simulate('change', {target: {value: 'event.type:error !ti'}}); mockCursorPosition(searchBar, 20); // use autocompletion to do the rest searchBar.onAutoComplete('title:', {}); expect(searchBar.state.query).toEqual('event.type:error !title:'); }); it('handles special case for user tag', function () { const props = { query: '', organization, location, supportedTags, }; const smartSearchBar = mountWithTheme(, options); const searchBar = smartSearchBar.instance(); const textarea = smartSearchBar.find('textarea'); textarea.simulate('change', {target: {value: 'user:'}}); mockCursorPosition(searchBar, 5); searchBar.onAutoComplete('id:1', {}); expect(searchBar.state.query).toEqual('user:"id:1" '); }); }); it('quotes in predefined values with spaces when autocompleting', async function () { jest.useRealTimers(); const onSearch = jest.fn(); supportedTags.predefined = { key: 'predefined', name: 'predefined', predefined: true, values: ['predefined tag with spaces'], }; const props = { orgId: 'org-slug', projectId: '0', query: '', location, organization, supportedTags, onSearch, }; const searchBar = mountWithTheme( , options ); searchBar.find('textarea').simulate('focus'); searchBar .find('textarea') .simulate('change', {target: {value: 'predefined:predefined'}}); await tick(); const preventDefault = jest.fn(); searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'}); searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault}); await tick(); expect(searchBar.find('textarea').props().value).toEqual( 'predefined:"predefined tag with spaces" ' ); }); it('escapes quotes in predefined values properly when autocompleting', async function () { jest.useRealTimers(); const onSearch = jest.fn(); supportedTags.predefined = { key: 'predefined', name: 'predefined', predefined: true, values: ['"predefined" "tag" "with" "quotes"'], }; const props = { orgId: 'org-slug', projectId: '0', query: '', location, organization, supportedTags, onSearch, }; const searchBar = mountWithTheme( , options ); searchBar.find('textarea').simulate('focus'); searchBar .find('textarea') .simulate('change', {target: {value: 'predefined:predefined'}}); await tick(); const preventDefault = jest.fn(); searchBar.find('textarea').simulate('keyDown', {key: 'ArrowDown'}); searchBar.find('textarea').simulate('keyDown', {key: 'Enter', preventDefault}); await tick(); expect(searchBar.find('textarea').props().value).toEqual( 'predefined:"\\"predefined\\" \\"tag\\" \\"with\\" \\"quotes\\"" ' ); }); describe('quick actions', () => { it('delete first token', async () => { const props = { query: 'is:unresolved sdk.name:sentry-cocoa has:key', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.updateAutoCompleteItems(); mockCursorPosition(searchBar, 1); await tick(); const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete); expect(deleteAction).toBeDefined(); if (deleteAction) { searchBar.runShortcut(deleteAction); await tick(); expect(searchBar.state.query).toEqual('sdk.name:sentry-cocoa has:key'); } }); it('delete middle token', async () => { const props = { query: 'is:unresolved sdk.name:sentry-cocoa has:key', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.updateAutoCompleteItems(); mockCursorPosition(searchBar, 18); await tick(); const deleteAction = shortcuts.find(a => a.shortcutType === ShortcutType.Delete); expect(deleteAction).toBeDefined(); if (deleteAction) { searchBar.runShortcut(deleteAction); await tick(); expect(searchBar.state.query).toEqual('is:unresolved has:key'); } }); it('exclude token', async () => { const props = { query: 'is:unresolved sdk.name:sentry-cocoa has:key', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.updateAutoCompleteItems(); mockCursorPosition(searchBar, 18); await tick(); const excludeAction = shortcuts.find(shortcut => shortcut.text === 'Exclude'); expect(excludeAction).toBeDefined(); if (excludeAction) { searchBar.runShortcut(excludeAction); await tick(); expect(searchBar.state.query).toEqual( 'is:unresolved !sdk.name:sentry-cocoa has:key ' ); } }); it('include token', async () => { const props = { query: 'is:unresolved !sdk.name:sentry-cocoa has:key', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.updateAutoCompleteItems(); mockCursorPosition(searchBar, 18); await tick(); const includeAction = shortcuts.find(shortcut => shortcut.text === 'Include'); expect(includeAction).toBeDefined(); if (includeAction) { searchBar.runShortcut(includeAction); await tick(); expect(searchBar.state.query).toEqual( 'is:unresolved sdk.name:sentry-cocoa has:key ' ); } }); }); describe('Invalid field state', () => { it('Shows invalid field state when invalid field is used', async () => { const props = { query: 'invalid:', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options); const searchBarInst = searchBar.instance(); mockCursorPosition(searchBarInst, 8); searchBar.find('textarea').simulate('focus'); searchBarInst.updateAutoCompleteItems(); await tick(); expect(searchBarInst.state.searchGroups).toHaveLength(1); expect(searchBarInst.state.searchGroups[0].title).toEqual('Keys'); expect(searchBarInst.state.searchGroups[0].type).toEqual('invalid-tag'); expect(searchBar.text()).toContain("The field invalid isn't supported here"); }); it('Does not show invalid field state when valid field is used', async () => { const props = { query: 'is:', organization, location, supportedTags, }; const searchBar = mountWithTheme(, options); const searchBarInst = searchBar.instance(); mockCursorPosition(searchBarInst, 3); searchBarInst.updateAutoCompleteItems(); await tick(); expect(searchBar.text()).not.toContain("isn't supported here"); }); }); });