Browse Source

feat(recent-search): List recent searches in Issues search bar [SEN-352] (#12499)

It's at the top here because the items below are not interact-able. Will need some design/icon work from @Chrissy as a follow up. Also API does not currently filter by current search input.

![image](https://user-images.githubusercontent.com/79684/54790583-e30e1d80-4bf3-11e9-9090-0c18ebcf56aa.png)

## Tag Key
![image](https://user-images.githubusercontent.com/79684/54790653-1355bc00-4bf4-11e9-8fb7-184054ecde7e.png)

## Tag Value
![image](https://user-images.githubusercontent.com/79684/54790662-1c468d80-4bf4-11e9-9f5d-52257954962a.png)

Fixes SEN-352
Billy Vong 6 years ago
parent
commit
7286c7217c

+ 2 - 0
src/sentry/static/sentry/app/actionCreators/savedSearches.jsx

@@ -1,3 +1,4 @@
+import {MAX_RECENT_SEARCHES} from 'app/constants';
 import handleXhrErrorResponse from 'app/utils/handleXhrErrorResponse';
 
 export function fetchSavedSearches(api, orgId, useOrgSavedSearches = false) {
@@ -63,6 +64,7 @@ export function fetchRecentSearches(api, orgId, type, query) {
       query: {
         query,
         type,
+        limit: MAX_RECENT_SEARCHES,
       },
     })
     .catch(handleXhrErrorResponse('Unable to fetch recent searches'));

+ 137 - 51
src/sentry/static/sentry/app/components/smartSearchBar.jsx

@@ -1,13 +1,15 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import Reflux from 'reflux';
+import * as Sentry from '@sentry/browser';
 import _ from 'lodash';
 import classNames from 'classnames';
 import createReactClass from 'create-react-class';
 import styled from 'react-emotion';
 
 import {NEGATION_OPERATOR, SEARCH_WILDCARD} from 'app/constants';
-import {saveRecentSearch} from 'app/actionCreators/savedSearches';
+import {defined} from 'app/utils';
+import {fetchRecentSearches, saveRecentSearch} from 'app/actionCreators/savedSearches';
 import {t} from 'app/locale';
 import MemberListStore from 'app/stores/memberListStore';
 import SearchDropdown from 'app/views/stream/searchDropdown';
@@ -63,6 +65,9 @@ class SmartSearchBar extends React.Component {
     // or a falsey value for no maximum
     maxSearchItems: PropTypes.number,
 
+    // List user's recent searches
+    displayRecentSearches: PropTypes.bool,
+
     /**
      * If this is defined, attempt to save search term scoped to the user and the current org
      */
@@ -71,8 +76,13 @@ class SmartSearchBar extends React.Component {
     // Callback that returns a promise of an array of strings
     onGetTagValues: PropTypes.func,
 
+    // Callback that returns a promise of an array of strings
+    onGetRecentSearches: PropTypes.func,
+
     onSearch: PropTypes.func,
 
+    onSavedRecentSearch: PropTypes.func,
+
     // If true, excludes the environment tag from the autocompletion list
     // This is because we don't want to treat environment as a tag in some places
     // such as the stream view where it is a top level concept
@@ -153,9 +163,12 @@ class SmartSearchBar extends React.Component {
   };
 
   onSubmit = evt => {
-    const {onSearch, api, orgId, recentSearchType} = this.props;
-
     evt.preventDefault();
+    this.doSearch();
+  };
+
+  doSearch = async () => {
+    const {onSearch, onSavedRecentSearch, api, orgId, recentSearchType} = this.props;
     this.blur();
     const query = removeSpace(this.state.query);
     onSearch(query);
@@ -163,8 +176,16 @@ class SmartSearchBar extends React.Component {
     // Only save recent search query if we have a recentSearchType (also 0 is a valid value)
     // Do not save empty string queries (i.e. if they clear search)
     if (typeof recentSearchType !== 'undefined' && query) {
-      saveRecentSearch(api, orgId, recentSearchType, query);
-      // Ignore errors if it fails to save
+      try {
+        await saveRecentSearch(api, orgId, recentSearchType, query);
+
+        if (onSavedRecentSearch) {
+          onSavedRecentSearch(query);
+        }
+      } catch (err) {
+        // Silently capture errors if it fails to save
+        Sentry.captureException(err);
+      }
     }
   };
 
@@ -210,7 +231,7 @@ class SmartSearchBar extends React.Component {
    *
    * e.g. ['is:', 'assigned:', 'url:', 'release:']
    */
-  getTagKeys = function(query) {
+  getTagKeys = query => {
     const {supportedTags, prepareQuery} = this.props;
 
     // Return all if query is empty
@@ -235,47 +256,77 @@ class SmartSearchBar extends React.Component {
    * Returns array of tag values that substring match `query`; invokes `callback`
    * with data when ready
    */
-  getTagValues = _.debounce((tag, query, callback) => {
-    // Strip double quotes if there are any
-    query = query.replace(/"/g, '').trim();
+  getTagValues = _.debounce(
+    async (tag, query) => {
+      // Strip double quotes if there are any
+      query = query.replace(/"/g, '').trim();
 
-    this.setState({
-      loading: true,
-    });
+      this.setState({
+        loading: true,
+      });
 
-    this.props.onGetTagValues(tag, query).then(
-      values => {
+      try {
+        const values = await this.props.onGetTagValues(tag, query);
         this.setState({loading: false});
-        callback(
-          values.map(value => {
-            // Wrap in quotes if there is a space
-            return value.indexOf(' ') > -1 ? `"${value}"` : value;
-          }),
-          tag.key,
-          query
-        );
-      },
-      () => {
+        return values.map(value => {
+          // Wrap in quotes if there is a space
+          return value.indexOf(' ') > -1 ? `"${value}"` : value;
+        });
+      } catch (err) {
         this.setState({loading: false});
+        Sentry.captureException(err);
+
+        return [];
       }
-    );
-  }, 300);
+    },
+    300,
+    {leading: true}
+  );
 
   /**
    * Returns array of tag values that substring match `query`; invokes `callback`
    * with results
    */
-  getPredefinedTagValues = function(tag, query, callback) {
-    const values = tag.values.filter(value => value.indexOf(query) > -1);
+  getPredefinedTagValues = function(tag, query) {
+    return tag.values.filter(value => value.indexOf(query) > -1);
+  };
 
-    callback(values, tag.key);
+  /**
+   * Get recent searches
+   */
+  getRecentSearches = _.debounce(
+    async () => {
+      const {recentSearchType, displayRecentSearches, onGetRecentSearches} = this.props;
+      // `recentSearchType` can be 0
+      if (!defined(recentSearchType) || !displayRecentSearches) {
+        return [];
+      }
+
+      const fetchFn = onGetRecentSearches || this.fetchRecentSearches;
+      return fetchFn(this.state.query);
+    },
+    300,
+    {leading: true}
+  );
+
+  fetchRecentSearches = async fullQuery => {
+    const {api, orgId, recentSearchType} = this.props;
+
+    const recentSearches = await fetchRecentSearches(
+      api,
+      orgId,
+      recentSearchType,
+      fullQuery
+    );
+
+    return (recentSearches && recentSearches.map(({query}) => ({query}))) || [];
   };
 
   onInputClick = () => {
     this.updateAutoCompleteItems();
   };
 
-  updateAutoCompleteItems = () => {
+  updateAutoCompleteItems = async () => {
     if (this.blurTimeout) {
       clearTimeout(this.blurTimeout);
       this.blurTimeout = null;
@@ -302,16 +353,22 @@ class SmartSearchBar extends React.Component {
         this.setState({
           searchTerm: query,
         });
-        return this.updateAutoCompleteState(this.getTagKeys(''), '');
+
+        const tagKeys = this.getTagKeys('');
+        const recentSearches = await this.getRecentSearches();
+        this.updateAutoCompleteState(tagKeys, recentSearches, '');
+        return;
       }
 
       // cursor on whitespace
       // show default "help" search terms
-      return this.setState({
+      this.setState({
         searchTerm: '',
         searchItems: defaultSearchItems,
         activeSearchItem: 0,
       });
+
+      return;
     }
 
     const last = terms.pop();
@@ -325,9 +382,10 @@ class SmartSearchBar extends React.Component {
       matchValue = last.replace(new RegExp(`^${NEGATION_OPERATOR}`), '');
 
       autoCompleteItems = this.getTagKeys(matchValue);
+      const recentSearches = await this.getRecentSearches();
 
       this.setState({searchTerm: matchValue});
-      this.updateAutoCompleteState(autoCompleteItems, matchValue);
+      this.updateAutoCompleteState(autoCompleteItems, recentSearches, matchValue);
     } else {
       const {supportedTags, prepareQuery} = this.props;
 
@@ -356,28 +414,32 @@ class SmartSearchBar extends React.Component {
       const tag = supportedTags[tagName];
 
       if (!tag) {
-        return undefined;
+        return;
       }
 
       // Ignore the environment tag if the feature is active and excludeEnvironment = true
       if (this.props.excludeEnvironment && tagName === 'environment') {
-        return undefined;
+        return;
       }
 
-      return (tag.predefined ? this.getPredefinedTagValues : this.getTagValues)(
-        tag,
-        preparedQuery,
-        this.updateAutoCompleteState
-      );
+      const fetchTagValuesFn = tag.predefined
+        ? this.getPredefinedTagValues
+        : this.getTagValues;
+
+      const [tagValues, recentSearches] = await Promise.all([
+        fetchTagValuesFn(tag, preparedQuery),
+        this.getRecentSearches(),
+      ]);
+
+      this.updateAutoCompleteState(tagValues, recentSearches, tag.key);
+      return;
     }
-    return undefined;
+    return;
   };
 
-  isDefaultDropdown = () => {
-    return this.state.searchItems === this.props.defaultSearchItems;
-  };
+  isDefaultDropdownItem = item => item.type === 'default';
 
-  updateAutoCompleteState = (searchItems, tagName) => {
+  updateAutoCompleteState = (searchItems, recentSearchItems, tagName) => {
     const {maxSearchItems} = this.props;
 
     searchItems = searchItems.map(item => {
@@ -398,15 +460,20 @@ class SmartSearchBar extends React.Component {
         case 'firstSeen':
         case 'lastSeen':
         case 'event.timestamp':
-          out.className = 'icon-clock';
+          out.className = 'icon-av_timer';
           break;
         default:
           out.className = 'icon-tag';
       }
+
+      if (item.type === 'recent-search') {
+        out.className = 'icon-clock';
+      }
+
       return out;
     });
 
-    if (searchItems.length > 0 && !this.isDefaultDropdown()) {
+    if (searchItems.length > 0) {
       searchItems[0].active = true;
     }
 
@@ -414,8 +481,15 @@ class SmartSearchBar extends React.Component {
       searchItems = searchItems.slice(0, maxSearchItems);
     }
 
+    const recentItems = recentSearchItems.map(item => ({
+      desc: item.query,
+      value: item.query,
+      className: 'icon-clock',
+      type: 'recent-search',
+    }));
+
     this.setState({
-      searchItems,
+      searchItems: [...searchItems, ...recentItems],
       activeSearchItem: 0,
     });
   };
@@ -441,14 +515,26 @@ class SmartSearchBar extends React.Component {
 
       searchItems[state.activeSearchItem].active = true;
       this.setState({searchItems: searchItems.slice(0)});
-    } else if (evt.key === 'Tab' && !this.isDefaultDropdown()) {
+    } else if (evt.key === 'Tab') {
       evt.preventDefault();
+      const item = searchItems[state.activeSearchItem];
 
-      this.onAutoComplete(searchItems[state.activeSearchItem].value);
+      if (!this.isDefaultDropdownItem(item)) {
+        this.onAutoComplete(item.value, item);
+      }
     }
   };
 
-  onAutoComplete = replaceText => {
+  onAutoComplete = (replaceText, item) => {
+    if (item.type === 'recent-search') {
+      this.setState({query: replaceText}, () => {
+        // Propagate onSearch and save to recent searches
+        this.doSearch();
+      });
+
+      return;
+    }
+
     const cursor = this.getCursorPosition();
     const query = this.state.query;
 

+ 1 - 0
src/sentry/static/sentry/app/constants/index.jsx

@@ -152,3 +152,4 @@ export const RECENT_SEARCH_TYPES = {
   ISSUE: 0,
   EVENT: 1,
 };
+export const MAX_RECENT_SEARCHES = 3;

+ 79 - 3
src/sentry/static/sentry/app/views/stream/searchBar.jsx

@@ -2,8 +2,12 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import {RECENT_SEARCH_TYPES} from 'app/constants';
+import {fetchRecentSearches} from 'app/actionCreators/savedSearches';
 import {t} from 'app/locale';
+import SentryTypes from 'app/sentryTypes';
 import SmartSearchBar from 'app/components/smartSearchBar';
+import withApi from 'app/utils/withApi';
+import withOrganization from 'app/utils/withOrganization';
 
 const SEARCH_ITEMS = [
   {
@@ -12,6 +16,7 @@ const SEARCH_ITEMS = [
     example: 'browser:"Chrome 34", has:browser',
     className: 'icon-tag',
     value: 'browser:',
+    type: 'default',
   },
   {
     title: t('Status'),
@@ -19,13 +24,15 @@ const SEARCH_ITEMS = [
     example: 'is:resolved, unresolved, ignored, assigned, unassigned',
     className: 'icon-toggle',
     value: 'is:',
+    type: 'default',
   },
   {
     title: t('Time or Count'),
     desc: t('Time or Count related search'),
     example: 'firstSeen, lastSeen, event.timestamp, timesSeen',
-    className: 'icon-clock',
+    className: 'icon-av_timer',
     value: '',
+    type: 'default',
   },
   {
     title: t('Assigned'),
@@ -33,6 +40,7 @@ const SEARCH_ITEMS = [
     example: 'assigned:[me|user@example.com]',
     className: 'icon-user',
     value: 'assigned:',
+    type: 'default',
   },
   {
     title: t('Bookmarked By'),
@@ -40,11 +48,13 @@ const SEARCH_ITEMS = [
     example: 'bookmarks:[me|user@example.com]',
     className: 'icon-user',
     value: 'bookmarks:',
+    type: 'default',
   },
   {
     desc: t('or paste an event id to jump straight to it'),
     className: 'icon-hash',
     value: '',
+    type: 'default',
   },
 ];
 
@@ -52,9 +62,52 @@ class SearchBar extends React.Component {
   static propTypes = {
     ...SmartSearchBar.propTypes,
 
+    organization: SentryTypes.Organization.isRequired,
     tagValueLoader: PropTypes.func.isRequired,
   };
 
+  state = {
+    defaultSearchItems: SEARCH_ITEMS,
+    recentSearches: [],
+  };
+
+  componentDidMount() {
+    // Ideally, we would fetch on demand (e.g. when input gets focus)
+    // but `<SmartSearchBar>` is a bit complicated and this is the easiest route
+    this.fetchData();
+  }
+
+  hasRecentSearches = () => {
+    const {organization} = this.props;
+    return organization && organization.features.includes('recent-searches');
+  };
+
+  fetchData = async () => {
+    if (!this.hasRecentSearches()) {
+      this.setState({
+        defaultSearchItems: SEARCH_ITEMS,
+      });
+
+      return;
+    }
+
+    const resp = await this.getRecentSearches();
+
+    this.setState({
+      defaultSearchItems: [
+        ...(resp &&
+          resp.map(query => ({
+            desc: query,
+            value: query,
+            className: 'icon-clock',
+            type: 'recent-search',
+          }))),
+        ...SEARCH_ITEMS,
+      ],
+      recentSearches: resp,
+    });
+  };
+
   /**
    * Returns array of tag values that substring match `query`; invokes `callback`
    * with data when ready
@@ -70,6 +123,27 @@ class SearchBar extends React.Component {
     );
   };
 
+  getRecentSearches = async fullQuery => {
+    const {api, orgId} = this.props;
+    const recent = await fetchRecentSearches(
+      api,
+      orgId,
+      RECENT_SEARCH_TYPES.ISSUE,
+      fullQuery
+    );
+    return (recent && recent.map(({query}) => query)) || [];
+  };
+
+  handleSavedRecentSearch = () => {
+    // No need to refetch if recent searches feature is not enabled
+    if (!this.hasRecentSearches()) {
+      return;
+    }
+
+    // Reset recent searches
+    this.fetchData();
+  };
+
   render() {
     const {
       tagValueLoader, // eslint-disable-line no-unused-vars
@@ -79,13 +153,15 @@ class SearchBar extends React.Component {
     return (
       <SmartSearchBar
         onGetTagValues={this.getTagValues}
-        defaultSearchItems={SEARCH_ITEMS}
+        defaultSearchItems={this.state.defaultSearchItems}
         maxSearchItems={5}
         recentSearchType={RECENT_SEARCH_TYPES.ISSUE}
+        displayRecentSearches={this.hasRecentSearches()}
+        onSavedRecentSearch={this.handleSavedRecentSearch}
         {...props}
       />
     );
   }
 }
 
-export default SearchBar;
+export default withApi(withOrganization(SearchBar));

+ 1 - 5
src/sentry/static/sentry/app/views/stream/searchDropdown.jsx

@@ -17,10 +17,6 @@ class SearchDropdown extends React.PureComponent {
     onClick: function() {},
   };
 
-  onClick = itemValue => {
-    this.props.onClick(itemValue);
-  };
-
   renderDescription = item => {
     const searchSubstring = this.props.searchSubstring;
     if (!searchSubstring) {
@@ -60,7 +56,7 @@ class SearchDropdown extends React.PureComponent {
                     'search-autocomplete-item',
                     item.active && 'active'
                   )}
-                  onClick={this.onClick.bind(this, item.value)}
+                  onClick={this.props.onClick.bind(this, item.value, item)}
                 >
                   <span className={classNames('icon', item.className)} />
                   <h4>

+ 18 - 6
tests/js/spec/components/smartSearchBar.spec.jsx

@@ -290,45 +290,57 @@ describe('SmartSearchBar', function() {
       expect(searchBar.state.activeSearchItem).toEqual(0);
     });
 
-    it('sets state when incomplete tag', function() {
+    it('sets state when incomplete tag', async function() {
       const props = {
         orgId: '123',
         projectId: '456',
         query: 'fu',
         supportedTags,
       };
-      const searchBar = mount(<SmartSearchBar {...props} />, options).instance();
+      jest.useRealTimers();
+      const wrapper = mount(<SmartSearchBar {...props} />, options);
+      const searchBar = wrapper.instance();
       searchBar.updateAutoCompleteItems();
+      await tick();
+      wrapper.update();
       expect(searchBar.state.searchTerm).toEqual('fu');
       expect(searchBar.state.searchItems).toEqual([]);
       expect(searchBar.state.activeSearchItem).toEqual(0);
     });
 
-    it('sets state when incomplete tag has negation operator', function() {
+    it('sets state when incomplete tag has negation operator', async function() {
       const props = {
         orgId: '123',
         projectId: '456',
         query: '!fu',
         supportedTags,
       };
-      const searchBar = mount(<SmartSearchBar {...props} />, options).instance();
+      jest.useRealTimers();
+      const wrapper = mount(<SmartSearchBar {...props} />, options);
+      const searchBar = wrapper.instance();
       searchBar.updateAutoCompleteItems();
+      await tick();
+      wrapper.update();
       expect(searchBar.state.searchTerm).toEqual('fu');
       expect(searchBar.state.searchItems).toEqual([]);
       expect(searchBar.state.activeSearchItem).toEqual(0);
     });
 
-    it('sets state when incomplete tag as second input', function() {
+    it('sets state when incomplete tag as second input', async function() {
       const props = {
         orgId: '123',
         projectId: '456',
         query: 'is:unresolved fu',
         supportedTags,
       };
-      const searchBar = mount(<SmartSearchBar {...props} />, options).instance();
+      jest.useRealTimers();
+      const wrapper = mount(<SmartSearchBar {...props} />, options);
+      const searchBar = wrapper.instance();
       searchBar.getCursorPosition = jest.fn();
       searchBar.getCursorPosition.mockReturnValue(15); // end of line
       searchBar.updateAutoCompleteItems();
+      await tick();
+      wrapper.update();
       expect(searchBar.state.searchTerm).toEqual('fu');
       expect(searchBar.state.searchItems).toHaveLength(0);
       expect(searchBar.state.activeSearchItem).toEqual(0);

+ 99 - 40
tests/js/spec/views/stream/searchBar.spec.jsx

@@ -8,6 +8,7 @@ describe('SearchBar', function() {
   let options;
   let tagValuePromise;
   let supportedTags;
+  let recentSearchMock;
   const clickInput = searchBar => searchBar.find('input[name="query"]').simulate('click');
 
   beforeEach(function() {
@@ -20,6 +21,11 @@ describe('SearchBar', function() {
     };
 
     tagValuePromise = Promise.resolve([]);
+
+    recentSearchMock = MockApiClient.addMockResponse({
+      url: '/organizations/123/recent-searches/',
+      body: [],
+    });
   });
 
   afterEach(function() {
@@ -31,6 +37,10 @@ describe('SearchBar', function() {
       jest.useFakeTimers();
     });
 
+    afterAll(function() {
+      jest.useRealTimers();
+    });
+
     it('sets state with complete tag', function() {
       const loader = (key, value) => {
         expect(key).toEqual('url');
@@ -96,46 +106,95 @@ describe('SearchBar', function() {
     });
   });
 
-  it('saves search query as a recent search', async function() {
-    jest.useFakeTimers();
-    const saveRecentSearch = MockApiClient.addMockResponse({
-      url: '/organizations/123/recent-searches/',
-      method: 'POST',
-      body: {},
+  describe('Recent Searches', function() {
+    it('saves search query as a recent search', async function() {
+      jest.useFakeTimers();
+      const saveRecentSearch = MockApiClient.addMockResponse({
+        url: '/organizations/123/recent-searches/',
+        method: 'POST',
+        body: {},
+      });
+      const loader = (key, value) => {
+        expect(key).toEqual('url');
+        expect(value).toEqual('fu');
+        return tagValuePromise;
+      };
+      const onSearch = jest.fn();
+      const props = {
+        orgId: '123',
+        query: 'url:"fu"',
+        onSearch,
+        tagValueLoader: loader,
+        supportedTags,
+      };
+      const searchBar = mount(<SearchBar {...props} />, options);
+      clickInput(searchBar);
+      jest.advanceTimersByTime(301);
+      expect(searchBar.find('SearchDropdown').prop('searchSubstring')).toEqual('"fu"');
+      expect(searchBar.find('SearchDropdown').prop('items')).toEqual([]);
+
+      jest.useRealTimers();
+      searchBar.find('form').simulate('submit');
+      expect(onSearch).toHaveBeenCalledWith('url:"fu"');
+
+      await tick();
+      searchBar.update();
+      expect(saveRecentSearch).toHaveBeenCalledWith(
+        expect.anything(),
+        expect.objectContaining({
+          data: {
+            query: 'url:"fu"',
+            type: 0,
+          },
+        })
+      );
+    });
+    it('does not query for recent searches if `displayRecentSearches` is `false`', async function() {
+      const props = {
+        orgId: '123',
+        query: 'timesSeen:',
+        tagValueLoader: () => {},
+        recentSearchType: 0,
+        displayRecentSearches: false,
+        supportedTags,
+      };
+      jest.useRealTimers();
+      const wrapper = mount(<SearchBar {...props} />, options);
+
+      wrapper.find('input').simulate('change', {target: {value: 'is:'}});
+
+      await tick();
+      wrapper.update();
+
+      expect(recentSearchMock).not.toHaveBeenCalled();
+    });
+
+    it('queries for recent searches if `displayRecentSearches` is `true`', async function() {
+      const props = {
+        orgId: '123',
+        query: 'timesSeen:',
+        tagValueLoader: () => {},
+        recentSearchType: 0,
+        displayRecentSearches: true,
+        supportedTags,
+      };
+      jest.useRealTimers();
+      const wrapper = mount(<SearchBar {...props} />, options);
+
+      wrapper.find('input').simulate('change', {target: {value: 'is:'}});
+      await tick();
+      wrapper.update();
+
+      expect(recentSearchMock).toHaveBeenCalledWith(
+        expect.anything(),
+        expect.objectContaining({
+          query: {
+            query: 'is:',
+            limit: 3,
+            type: 0,
+          },
+        })
+      );
     });
-    const loader = (key, value) => {
-      expect(key).toEqual('url');
-      expect(value).toEqual('fu');
-      return tagValuePromise;
-    };
-    const onSearch = jest.fn();
-    const props = {
-      orgId: '123',
-      query: 'url:"fu"',
-      onSearch,
-      tagValueLoader: loader,
-      supportedTags,
-    };
-    const searchBar = mount(<SearchBar {...props} />, options);
-    clickInput(searchBar);
-    jest.advanceTimersByTime(301);
-    expect(searchBar.find('SearchDropdown').prop('searchSubstring')).toEqual('"fu"');
-    expect(searchBar.find('SearchDropdown').prop('items')).toEqual([]);
-
-    jest.useRealTimers();
-    searchBar.find('form').simulate('submit');
-    expect(onSearch).toHaveBeenCalledWith('url:"fu"');
-
-    await tick();
-    searchBar.update();
-    expect(saveRecentSearch).toHaveBeenCalledWith(
-      expect.anything(),
-      expect.objectContaining({
-        data: {
-          query: 'url:"fu"',
-          type: 0,
-        },
-      })
-    );
   });
 });