savedIssueSearches.spec.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import {ComponentProps, Fragment} from 'react';
  2. import {
  3. render,
  4. renderGlobalModal,
  5. screen,
  6. userEvent,
  7. waitFor,
  8. waitForElementToBeRemoved,
  9. within,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import GlobalModalContainer from 'sentry/components/globalModal';
  12. import {SavedSearchVisibility} from 'sentry/types';
  13. import localStorageWrapper from 'sentry/utils/localStorage';
  14. import SavedIssueSearches from 'sentry/views/issueList/savedIssueSearches';
  15. import {SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY} from 'sentry/views/issueList/utils';
  16. describe('SavedIssueSearches', function () {
  17. const organization = TestStubs.Organization();
  18. const recommendedSearch = TestStubs.Search({
  19. id: 'global-search',
  20. isGlobal: true,
  21. name: 'Assigned to Me',
  22. query: 'is:unresolved assigned:me',
  23. visibility: SavedSearchVisibility.Organization,
  24. });
  25. const userSearch = TestStubs.Search({
  26. id: 'user-search',
  27. isGlobal: false,
  28. name: 'Just Firefox',
  29. query: 'browser:firefox',
  30. visibility: SavedSearchVisibility.Owner,
  31. });
  32. const orgSearch = TestStubs.Search({
  33. id: 'org-search',
  34. isGlobal: false,
  35. name: 'Last 4 Hours',
  36. query: 'age:-4h',
  37. visibility: SavedSearchVisibility.Organization,
  38. });
  39. const pinnedSearch = TestStubs.Search({
  40. id: 'pinned-search',
  41. isGlobal: false,
  42. isPinned: true,
  43. name: 'My Pinned Search',
  44. query: 'age:-4h',
  45. });
  46. const defaultProps: ComponentProps<typeof SavedIssueSearches> = {
  47. organization,
  48. onSavedSearchSelect: jest.fn(),
  49. query: 'is:unresolved',
  50. sort: 'date',
  51. };
  52. beforeEach(() => {
  53. localStorageWrapper.setItem(SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY, 'true');
  54. MockApiClient.clearMockResponses();
  55. jest.restoreAllMocks();
  56. });
  57. it('displays saved searches with correct text and in correct sections', async function () {
  58. MockApiClient.addMockResponse({
  59. url: '/organizations/org-slug/searches/',
  60. body: [userSearch, recommendedSearch, orgSearch, pinnedSearch],
  61. });
  62. const {container} = render(<SavedIssueSearches {...defaultProps} />);
  63. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  64. expect(container).toSnapshot();
  65. });
  66. it('hides saves searches by default past first 4', async function () {
  67. MockApiClient.addMockResponse({
  68. url: '/organizations/org-slug/searches/',
  69. body: [...new Array(6)].map((_, i) => ({
  70. ...orgSearch,
  71. name: 'Test Search',
  72. id: i,
  73. })),
  74. });
  75. render(<SavedIssueSearches {...defaultProps} />);
  76. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  77. expect(screen.getAllByText('Test Search')).toHaveLength(4);
  78. userEvent.click(screen.getByRole('button', {name: /show 2 more/i}));
  79. expect(screen.getAllByText('Test Search')).toHaveLength(6);
  80. });
  81. it('can select a saved search', async function () {
  82. MockApiClient.addMockResponse({
  83. url: '/organizations/org-slug/searches/',
  84. body: [recommendedSearch, orgSearch, pinnedSearch],
  85. });
  86. render(<SavedIssueSearches {...defaultProps} />);
  87. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  88. userEvent.click(screen.getByRole('button', {name: 'Assigned to Me'}));
  89. expect(defaultProps.onSavedSearchSelect).toHaveBeenLastCalledWith(recommendedSearch);
  90. userEvent.click(screen.getByRole('button', {name: 'Last 4 Hours'}));
  91. expect(defaultProps.onSavedSearchSelect).toHaveBeenLastCalledWith(orgSearch);
  92. });
  93. it('does not show header when there are no org saved searches', async function () {
  94. MockApiClient.addMockResponse({
  95. url: '/organizations/org-slug/searches/',
  96. body: [recommendedSearch],
  97. });
  98. render(<SavedIssueSearches {...defaultProps} />);
  99. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  100. expect(screen.getByText(/You don't have any saved searches/i)).toBeInTheDocument();
  101. });
  102. it('does not show overflow menu for recommended searches', async function () {
  103. MockApiClient.addMockResponse({
  104. url: '/organizations/org-slug/searches/',
  105. body: [recommendedSearch],
  106. });
  107. render(<SavedIssueSearches {...defaultProps} />);
  108. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  109. expect(
  110. screen.queryByRole('button', {name: /saved search options/i})
  111. ).not.toBeInTheDocument();
  112. });
  113. it('can delete an org saved search with correct permissions', async function () {
  114. MockApiClient.addMockResponse({
  115. url: '/organizations/org-slug/searches/',
  116. body: [recommendedSearch, orgSearch, pinnedSearch],
  117. });
  118. const deleteMock = MockApiClient.addMockResponse({
  119. url: '/organizations/org-slug/searches/org-search/',
  120. method: 'DELETE',
  121. });
  122. render(<SavedIssueSearches {...defaultProps} />);
  123. renderGlobalModal();
  124. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  125. userEvent.click(screen.getByRole('button', {name: /saved search options/i}));
  126. userEvent.click(screen.getByRole('menuitemradio', {name: /delete/i}));
  127. const modal = screen.getByRole('dialog');
  128. expect(within(modal).getByText(/are you sure/i)).toBeInTheDocument();
  129. userEvent.click(within(modal).getByRole('button', {name: /confirm/i}));
  130. await waitFor(() => {
  131. expect(deleteMock).toHaveBeenCalledTimes(1);
  132. expect(screen.queryByText(orgSearch.name)).not.toBeInTheDocument();
  133. });
  134. });
  135. it('can edit an org saved search with correct permissions', async function () {
  136. MockApiClient.addMockResponse({
  137. url: '/organizations/org-slug/searches/',
  138. body: [recommendedSearch, orgSearch, pinnedSearch],
  139. });
  140. const putMock = MockApiClient.addMockResponse({
  141. url: '/organizations/org-slug/searches/org-search/',
  142. method: 'PUT',
  143. body: {
  144. ...orgSearch,
  145. name: 'new name',
  146. },
  147. });
  148. render(
  149. <Fragment>
  150. <SavedIssueSearches {...defaultProps} />
  151. <GlobalModalContainer />
  152. </Fragment>
  153. );
  154. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  155. userEvent.click(screen.getByRole('button', {name: /saved search options/i}));
  156. userEvent.click(screen.getByRole('menuitemradio', {name: /edit/i}));
  157. const modal = screen.getByRole('dialog');
  158. userEvent.clear(within(modal).getByRole('textbox', {name: /name/i}));
  159. userEvent.type(within(modal).getByRole('textbox', {name: /name/i}), 'new name');
  160. userEvent.click(within(modal).getByRole('button', {name: /save/i}));
  161. await waitFor(() => {
  162. expect(putMock).toHaveBeenCalledWith(
  163. expect.anything(),
  164. expect.objectContaining({
  165. data: expect.objectContaining({
  166. name: 'new name',
  167. }),
  168. })
  169. );
  170. expect(screen.getByText('new name')).toBeInTheDocument();
  171. });
  172. });
  173. it('cannot delete or edit a saved search without correct permissions', async function () {
  174. MockApiClient.addMockResponse({
  175. url: '/organizations/org-slug/searches/',
  176. body: [recommendedSearch, orgSearch, pinnedSearch],
  177. });
  178. render(
  179. <SavedIssueSearches
  180. {...defaultProps}
  181. organization={{
  182. ...organization,
  183. access: organization.access.filter(access => access !== 'org:write'),
  184. }}
  185. />
  186. );
  187. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  188. userEvent.click(screen.getByRole('button', {name: /saved search options/i}));
  189. expect(
  190. screen.getByText('You do not have permission to delete this search.')
  191. ).toBeInTheDocument();
  192. expect(
  193. screen.getByText('You do not have permission to edit this search.')
  194. ).toBeInTheDocument();
  195. });
  196. it('can create a new saved search', async function () {
  197. MockApiClient.addMockResponse({
  198. url: '/organizations/org-slug/searches/',
  199. body: [recommendedSearch],
  200. });
  201. const mockSave = MockApiClient.addMockResponse({
  202. url: '/organizations/org-slug/searches/',
  203. method: 'POST',
  204. body: {},
  205. });
  206. render(<SavedIssueSearches {...defaultProps} />);
  207. renderGlobalModal();
  208. await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator'));
  209. userEvent.click(screen.getByRole('button', {name: /create a new saved search/i}));
  210. const modal = screen.getByRole('dialog');
  211. userEvent.type(
  212. within(modal).getByRole('textbox', {name: /name/i}),
  213. 'new saved search'
  214. );
  215. userEvent.click(within(modal).getByRole('button', {name: /save/i}));
  216. await waitFor(() => {
  217. expect(mockSave).toHaveBeenCalledWith(
  218. expect.anything(),
  219. expect.objectContaining({
  220. data: expect.objectContaining({
  221. name: 'new saved search',
  222. query: 'is:unresolved',
  223. }),
  224. })
  225. );
  226. });
  227. // Modal should close
  228. await waitForElementToBeRemoved(() => screen.getByRole('dialog'));
  229. });
  230. });