savedIssueSearches.spec.tsx 8.4 KB

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