import React from 'react'; import {shallow, mountWithTheme} from 'sentry-test/enzyme'; import {Client} from 'app/api'; import {SmartSearchBar, addSpace, removeSpace} from 'app/components/smartSearchBar'; import TagStore from 'app/stores/tagStore'; describe('addSpace()', function() { it('should add a space when there is no trailing space', function() { expect(addSpace('one')).toEqual('one '); }); it('should not add another space when there is already one', function() { expect(addSpace('one ')).toEqual('one '); }); it('should leave the empty string alone', function() { expect(addSpace('')).toEqual(''); }); }); describe('removeSpace()', function() { it('should remove a trailing space', function() { expect(removeSpace('one ')).toEqual('one'); }); it('should not remove the last character if it is not a space', function() { expect(removeSpace('one')).toEqual('one'); }); it('should leave the empty string alone', function() { expect(removeSpace('')).toEqual(''); }); }); describe('SmartSearchBar', function() { let options, organization, supportedTags; let environmentTagValuesMock; const tagValuesMock = jest.fn(() => Promise.resolve([])); beforeEach(function() { TagStore.reset(); TagStore.onLoadTagsSuccess(TestStubs.Tags()); tagValuesMock.mockClear(); supportedTags = TagStore.getAllTags(); organization = TestStubs.Organization({id: '123'}); const 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(); }); describe('componentWillReceiveProps()', function() { it('should add a space when setting state.query', function() { const searchBar = shallow( , options ); expect(searchBar.state().query).toEqual('one '); }); it('should update state.query if props.query is updated from outside', function() { const searchBar = shallow( , options ); searchBar.setProps({query: 'two'}); expect(searchBar.state().query).toEqual('two '); }); it('should not reset user input if a noop props change happens', function() { const searchBar = shallow( , options ); searchBar.setState({query: 'two'}); searchBar.setProps({query: 'one'}); expect(searchBar.state().query).toEqual('two'); }); it('should reset user input if a meaningful props change happens', function() { const searchBar = shallow( , options ); searchBar.setState({query: 'two'}); searchBar.setProps({query: 'three'}); expect(searchBar.state().query).toEqual('three '); }); }); describe('getQueryTerms()', function() { it('should extract query terms from a query string', function() { let query = 'tagname: '; expect(SmartSearchBar.getQueryTerms(query, query.length)).toEqual(['tagname:']); query = 'tagname:derp browser:'; expect(SmartSearchBar.getQueryTerms(query, query.length)).toEqual([ 'tagname:derp', 'browser:', ]); query = ' browser:"Chrome 33.0" '; expect(SmartSearchBar.getQueryTerms(query, query.length)).toEqual([ 'browser:"Chrome 33.0"', ]); }); }); describe('getLastTermIndex()', function() { it('should provide the index of the last query term, given cursor index', function() { let query = 'tagname:'; expect(SmartSearchBar.getLastTermIndex(query, 0)).toEqual(8); query = 'tagname:foo'; // 'f' (index 9) expect(SmartSearchBar.getLastTermIndex(query, 9)).toEqual(11); query = 'tagname:foo anothertag:bar'; // 'f' (index 9) expect(SmartSearchBar.getLastTermIndex(query, 9)).toEqual(11); }); }); describe('clearSearch()', function() { it('clears the query', function() { const props = { organization, query: 'is:unresolved ruby', defaultQuery: 'is:unresolved', supportedTags, }; const searchBar = shallow(, options).instance(); searchBar.clearSearch(); expect(searchBar.state.query).toEqual(''); }); it('calls onSearch()', async function() { const props = { organization, query: 'is:unresolved ruby', defaultQuery: 'is:unresolved', supportedTags, onSearch: jest.fn(), }; const searchBar = shallow(, options).instance(); await searchBar.clearSearch(); expect(props.onSearch).toHaveBeenCalledWith(''); }); }); describe('onQueryFocus()', function() { it('displays the drop down', function() { const searchBar = shallow( , options ).instance(); expect(searchBar.state.dropdownVisible).toBe(false); searchBar.onQueryFocus(); expect(searchBar.state.dropdownVisible).toBe(true); }); it('displays dropdown in hasPinnedSearch mode', function() { const searchBar = shallow( , options ).instance(); expect(searchBar.state.dropdownVisible).toBe(false); searchBar.onQueryFocus(); expect(searchBar.state.dropdownVisible).toBe(true); }); }); describe('onQueryBlur()', function() { it('hides the drop down', function() { const searchBar = shallow( , options ).instance(); searchBar.state.dropdownVisible = true; jest.useFakeTimers(); searchBar.onQueryBlur(); jest.advanceTimersByTime(201); // doesn't close until 200ms expect(searchBar.state.dropdownVisible).toBe(false); }); }); describe('onKeyUp()', function() { describe('escape', function() { it('blurs the input', function() { const wrapper = mountWithTheme( , options ); wrapper.setState({dropdownVisible: true}); const instance = wrapper.instance(); jest.spyOn(instance, 'blur'); wrapper.find('input').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, 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, 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, supportedTags, }; const searchBar = mountWithTheme(, options).instance(); searchBar.updateAutoCompleteItems(); expect(searchBar.state.searchTerm).toEqual(''); expect(searchBar.state.searchItems).toEqual([]); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('sets state when incomplete tag', async function() { const props = { query: 'fu', organization, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); expect(searchBar.state.searchTerm).toEqual('fu'); expect(searchBar.state.searchItems).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, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, options); const searchBar = wrapper.instance(); searchBar.updateAutoCompleteItems(); await tick(); wrapper.update(); expect(searchBar.state.searchTerm).toEqual('fu'); expect(searchBar.state.searchItems).toEqual([ expect.objectContaining({children: []}), ]); expect(searchBar.state.activeSearchItem).toEqual(-1); }); it('sets state when incomplete tag as second input', async function() { const props = { query: 'is:unresolved fu', organization, supportedTags, }; jest.useRealTimers(); const wrapper = mountWithTheme(, 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'); // 1 items because of headers ("Tags") expect(searchBar.state.searchItems).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, 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:', organization, supportedTags, }; const searchBar = mountWithTheme( , options ).instance(); 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 }, }) ); }); }); describe('onTogglePinnedSearch', function() { let pinRequest, unpinRequest; beforeEach(function() { pinRequest = MockApiClient.addMockResponse({ url: '/organizations/org-slug/pinned-searches/', method: 'PUT', body: [], }); unpinRequest = MockApiClient.addMockResponse({ url: '/organizations/org-slug/pinned-searches/', method: 'DELETE', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', method: 'POST', body: {}, }); }); it('does not pin when query is empty', async function() { const wrapper = mountWithTheme( , options ); wrapper.find('button[aria-label="Pin this search"]').simulate('click'); await wrapper.update(); expect(pinRequest).not.toHaveBeenCalled(); }); it('adds pins', async function() { const wrapper = mountWithTheme( , options ); wrapper.find('button[aria-label="Pin this search"]').simulate('click'); await wrapper.update(); expect(pinRequest).toHaveBeenCalled(); expect(unpinRequest).not.toHaveBeenCalled(); }); it('removes pins', async function() { const pinnedSearch = TestStubs.Search({isPinned: true}); const wrapper = mountWithTheme( , options ); wrapper.find('button[aria-label="Unpin this search"]').simulate('click'); await wrapper.update(); expect(pinRequest).not.toHaveBeenCalled(); expect(unpinRequest).toHaveBeenCalled(); }); }); });