123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799 |
- import {mountWithTheme} from 'sentry-test/enzyme';
- import {Client} from 'sentry/api';
- import {SmartSearchBar} from 'sentry/components/smartSearchBar';
- 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.onLoadTagsSuccess(TestStubs.Tags());
- tagValuesMock.mockClear();
- supportedTags = TagStore.getAllTags();
- supportedTags.firstRelease = {
- key: 'firstRelease',
- name: 'firstRelease',
- };
- 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(
- <SmartSearchBar {...props} api={new Client()} />,
- 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(
- <SmartSearchBar {...props} api={new Client()} />,
- 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(
- <SmartSearchBar {...props} api={new Client()} />,
- 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(
- <SmartSearchBar {...props} api={new Client()} />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- query="one"
- />,
- options
- );
- expect(searchBar.state().query).toEqual('one ');
- });
- it('should update state.query if props.query is updated from outside', function () {
- const searchBar = mountWithTheme(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- query="one"
- />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- query="one"
- />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- query="one"
- />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- query="one"
- />,
- 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, options).instance();
- await searchBar.clearSearch();
- expect(props.onSearch).toHaveBeenCalledWith('');
- });
- });
- describe('onQueryFocus()', function () {
- it('displays the drop down', function () {
- const searchBar = mountWithTheme(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- onGetTagValues={tagValuesMock}
- />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- onGetTagValues={tagValuesMock}
- hasPinnedSearch
- />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- onChange={onChange}
- />,
- 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(
- <SmartSearchBar
- organization={organization}
- location={location}
- supportedTags={supportedTags}
- />,
- 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(
- <SmartSearchBar
- onSearch={stubbedOnSearch}
- organization={organization}
- location={location}
- query="is:unresolved"
- supportedTags={supportedTags}
- />,
- 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(<SmartSearchBar {...props} />, 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(
- <SmartSearchBar
- onSearch={stubbedOnSearch}
- organization={organization}
- query="is:unresolved"
- location={location}
- supportedTags={supportedTags}
- hasPinnedSearch
- />,
- 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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');
- // 1 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(<SmartSearchBar {...props} />, 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(
- <SmartSearchBar {...props} api={new Client()} />,
- 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(
- <SmartSearchBar {...props} api={new Client()} />,
- 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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 only showing tags now
- expect(searchBar.state.searchGroups).toHaveLength(1);
- expect(searchBar.state.activeSearchItem).toEqual(-1);
- });
- it('shows errors on incorrect tokens', async function () {
- const props = {
- query: 'tag: is: has: ',
- organization,
- location,
- supportedTags,
- };
- jest.useRealTimers();
- const wrapper = mountWithTheme(<SmartSearchBar {...props} />, options);
- wrapper.find('Filter').forEach(filter => {
- expect(filter.prop('invalid')).toBe(true);
- });
- });
- });
- describe('onAutoComplete()', function () {
- it('completes terms from the list', function () {
- const props = {
- query: 'event.type:error ',
- organization,
- location,
- supportedTags,
- };
- const searchBar = mountWithTheme(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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(
- <SmartSearchBar {...props} onChange={onChange} />,
- 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(<SmartSearchBar {...props} />, 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(<SmartSearchBar {...props} />, 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" ');
- });
- });
- });
|