apiSource.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import {ComponentProps} from 'react';
  2. import omit from 'lodash/omit';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {render, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import {ApiSource} from 'sentry/components/search/sources/apiSource';
  6. describe('ApiSource', function () {
  7. const {org, router} = initializeOrg();
  8. let orgsMock;
  9. let projectsMock;
  10. let teamsMock;
  11. let membersMock;
  12. let shortIdMock;
  13. let eventIdMock;
  14. const defaultProps: ComponentProps<typeof ApiSource> = {
  15. query: '',
  16. organization: org,
  17. router,
  18. location: router.location,
  19. routes: [],
  20. params: {orgId: org.slug},
  21. children: jest.fn().mockReturnValue(null),
  22. };
  23. beforeEach(function () {
  24. MockApiClient.clearMockResponses();
  25. MockApiClient.addMockResponse({
  26. url: '/organizations/',
  27. body: [TestStubs.Organization({slug: 'test-org'})],
  28. });
  29. orgsMock = MockApiClient.addMockResponse({
  30. url: '/organizations/',
  31. body: [TestStubs.Organization({slug: 'foo-org'})],
  32. });
  33. projectsMock = MockApiClient.addMockResponse({
  34. url: '/organizations/org-slug/projects/',
  35. body: [TestStubs.Project({slug: 'foo-project'})],
  36. });
  37. teamsMock = MockApiClient.addMockResponse({
  38. url: '/organizations/org-slug/teams/',
  39. body: [TestStubs.Team({slug: 'foo-team'})],
  40. });
  41. membersMock = MockApiClient.addMockResponse({
  42. url: '/organizations/org-slug/members/',
  43. body: TestStubs.Members(),
  44. });
  45. shortIdMock = MockApiClient.addMockResponse({
  46. url: '/organizations/org-slug/shortids/test-1/',
  47. body: TestStubs.ShortIdQueryResult(),
  48. });
  49. eventIdMock = MockApiClient.addMockResponse({
  50. url: '/organizations/org-slug/eventids/12345678901234567890123456789012/',
  51. body: TestStubs.EventIdQueryResult(),
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/plugins/?plugins=_all',
  55. body: [],
  56. });
  57. MockApiClient.addMockResponse({
  58. url: '/organizations/org-slug/plugins/configs/',
  59. body: [],
  60. });
  61. MockApiClient.addMockResponse({
  62. url: '/organizations/org-slug/config/integrations/',
  63. body: [],
  64. });
  65. MockApiClient.addMockResponse({
  66. url: '/sentry-apps/?status=published',
  67. body: [],
  68. });
  69. MockApiClient.addMockResponse({
  70. url: '/doc-integrations/',
  71. body: [],
  72. });
  73. MockApiClient.addMockResponse({
  74. url: '/organizations/org-slug/shortids/foo-t/',
  75. body: [],
  76. });
  77. });
  78. it('queries all API endpoints', function () {
  79. const mock = jest.fn().mockReturnValue(null);
  80. render(
  81. <ApiSource {...defaultProps} query="foo">
  82. {mock}
  83. </ApiSource>
  84. );
  85. expect(orgsMock).toHaveBeenCalled();
  86. expect(projectsMock).toHaveBeenCalled();
  87. expect(teamsMock).toHaveBeenCalled();
  88. expect(membersMock).toHaveBeenCalled();
  89. expect(shortIdMock).not.toHaveBeenCalled();
  90. expect(eventIdMock).not.toHaveBeenCalled();
  91. });
  92. it('only queries for shortids when query matches shortid format', async function () {
  93. const mock = jest.fn().mockReturnValue(null);
  94. const {rerender} = render(
  95. <ApiSource {...defaultProps} query="test-">
  96. {mock}
  97. </ApiSource>
  98. );
  99. expect(shortIdMock).not.toHaveBeenCalled();
  100. rerender(
  101. <ApiSource {...defaultProps} query="test-1">
  102. {mock}
  103. </ApiSource>
  104. );
  105. expect(shortIdMock).toHaveBeenCalled();
  106. // These may not be desired behavior in future, but lets specify the expectation regardless
  107. expect(orgsMock).toHaveBeenCalled();
  108. expect(projectsMock).toHaveBeenCalled();
  109. expect(teamsMock).toHaveBeenCalled();
  110. expect(membersMock).toHaveBeenCalled();
  111. expect(eventIdMock).not.toHaveBeenCalled();
  112. await waitFor(() => {
  113. expect(mock).toHaveBeenLastCalledWith(
  114. expect.objectContaining({
  115. results: [
  116. {
  117. item: expect.objectContaining({
  118. title: 'group type',
  119. description: 'group description',
  120. sourceType: 'issue',
  121. resultType: 'issue',
  122. to: '/org-slug/project-slug/issues/1/',
  123. }),
  124. score: 1,
  125. refIndex: 0,
  126. },
  127. ],
  128. })
  129. );
  130. });
  131. });
  132. it('only queries for eventids when query matches eventid format of 32 chars', async function () {
  133. const mock = jest.fn().mockReturnValue(null);
  134. const {rerender} = render(
  135. <ApiSource {...defaultProps} query="1234567890123456789012345678901">
  136. {mock}
  137. </ApiSource>
  138. );
  139. expect(eventIdMock).not.toHaveBeenCalled();
  140. rerender(
  141. // This is a valid short id now
  142. <ApiSource {...defaultProps} query="12345678901234567890123456789012">
  143. {mock}
  144. </ApiSource>
  145. );
  146. await waitFor(() => {
  147. expect(eventIdMock).toHaveBeenCalled();
  148. });
  149. // These may not be desired behavior in future, but lets specify the expectation regardless
  150. expect(orgsMock).toHaveBeenCalled();
  151. expect(projectsMock).toHaveBeenCalled();
  152. expect(teamsMock).toHaveBeenCalled();
  153. expect(membersMock).toHaveBeenCalled();
  154. expect(shortIdMock).not.toHaveBeenCalled();
  155. expect(mock).toHaveBeenLastCalledWith(
  156. expect.objectContaining({
  157. results: [
  158. {
  159. item: expect.objectContaining({
  160. title: 'event type',
  161. description: 'event description',
  162. sourceType: 'event',
  163. resultType: 'event',
  164. to: '/org-slug/project-slug/issues/1/events/12345678901234567890123456789012/',
  165. }),
  166. score: 1,
  167. refIndex: 0,
  168. },
  169. ],
  170. })
  171. );
  172. });
  173. it('only queries org endpoint if there is no org in context', function () {
  174. const mock = jest.fn().mockReturnValue(null);
  175. render(
  176. <ApiSource {...omit(defaultProps, 'organization')} params={{orgId: ''}} query="foo">
  177. {mock}
  178. </ApiSource>
  179. );
  180. expect(orgsMock).toHaveBeenCalled();
  181. expect(projectsMock).not.toHaveBeenCalled();
  182. expect(teamsMock).not.toHaveBeenCalled();
  183. expect(membersMock).not.toHaveBeenCalled();
  184. });
  185. it('render function is called with correct results', async function () {
  186. const mock = jest.fn().mockReturnValue(null);
  187. render(
  188. <ApiSource {...defaultProps} organization={org} query="foo">
  189. {mock}
  190. </ApiSource>
  191. );
  192. await waitFor(() => {
  193. expect(mock).toHaveBeenLastCalledWith({
  194. isLoading: false,
  195. results: expect.arrayContaining([
  196. expect.objectContaining({
  197. item: expect.objectContaining({
  198. model: expect.objectContaining({
  199. slug: 'foo-org',
  200. }),
  201. sourceType: 'organization',
  202. resultType: 'settings',
  203. to: '/settings/foo-org/',
  204. }),
  205. matches: expect.anything(),
  206. score: expect.anything(),
  207. }),
  208. expect.objectContaining({
  209. item: expect.objectContaining({
  210. model: expect.objectContaining({
  211. slug: 'foo-org',
  212. }),
  213. sourceType: 'organization',
  214. resultType: 'route',
  215. to: '/foo-org/',
  216. }),
  217. matches: expect.anything(),
  218. score: expect.anything(),
  219. }),
  220. expect.objectContaining({
  221. item: expect.objectContaining({
  222. model: expect.objectContaining({
  223. slug: 'foo-project',
  224. }),
  225. sourceType: 'project',
  226. resultType: 'route',
  227. to: '/organizations/org-slug/projects/foo-project/?project=2',
  228. }),
  229. matches: expect.anything(),
  230. score: expect.anything(),
  231. }),
  232. expect.objectContaining({
  233. item: expect.objectContaining({
  234. model: expect.objectContaining({
  235. slug: 'foo-project',
  236. }),
  237. sourceType: 'project',
  238. resultType: 'route',
  239. to: '/organizations/org-slug/alerts/rules/?project=2',
  240. }),
  241. matches: expect.anything(),
  242. score: expect.anything(),
  243. }),
  244. expect.objectContaining({
  245. item: expect.objectContaining({
  246. model: expect.objectContaining({
  247. slug: 'foo-project',
  248. }),
  249. sourceType: 'project',
  250. resultType: 'settings',
  251. to: '/settings/org-slug/projects/foo-project/',
  252. }),
  253. matches: expect.anything(),
  254. score: expect.anything(),
  255. }),
  256. expect.objectContaining({
  257. item: expect.objectContaining({
  258. model: expect.objectContaining({
  259. slug: 'foo-team',
  260. }),
  261. sourceType: 'team',
  262. resultType: 'settings',
  263. to: '/settings/org-slug/teams/foo-team/',
  264. }),
  265. matches: expect.anything(),
  266. score: expect.anything(),
  267. }),
  268. ]),
  269. });
  270. });
  271. // The return values here are because of fuzzy search matching.
  272. // There are no members that match
  273. expect(mock.mock.calls[1][0].results).toHaveLength(6);
  274. });
  275. it('render function is called with correct results when API requests partially succeed', async function () {
  276. const mock = jest.fn().mockReturnValue(null);
  277. MockApiClient.addMockResponse({
  278. url: '/organizations/org-slug/projects/',
  279. statusCode: 500,
  280. });
  281. render(
  282. <ApiSource {...defaultProps} query="foo">
  283. {mock}
  284. </ApiSource>
  285. );
  286. await waitFor(() => {
  287. expect(mock).toHaveBeenLastCalledWith({
  288. isLoading: false,
  289. results: expect.arrayContaining([
  290. expect.objectContaining({
  291. item: expect.objectContaining({
  292. model: expect.objectContaining({
  293. slug: 'foo-org',
  294. }),
  295. }),
  296. }),
  297. expect.objectContaining({
  298. item: expect.objectContaining({
  299. model: expect.objectContaining({
  300. slug: 'foo-org',
  301. }),
  302. }),
  303. }),
  304. expect.objectContaining({
  305. item: expect.objectContaining({
  306. model: expect.objectContaining({
  307. slug: 'foo-team',
  308. }),
  309. }),
  310. }),
  311. ]),
  312. });
  313. });
  314. // The return values here are because of fuzzy search matching.
  315. // There are no members that match
  316. expect(mock.mock.calls[1][0].results).toHaveLength(3);
  317. });
  318. it('render function is updated as query changes', async function () {
  319. const mock = jest.fn().mockReturnValue(null);
  320. const {rerender} = render(
  321. <ApiSource {...defaultProps} query="foo">
  322. {mock}
  323. </ApiSource>
  324. );
  325. await waitFor(() => {
  326. // The return values here are because of fuzzy search matching.
  327. // There are no members that match
  328. expect(mock.mock.calls[1][0].results).toHaveLength(6);
  329. expect(mock.mock.calls[1][0].results[0].item.model.slug).toBe('foo-org');
  330. });
  331. mock.mockClear();
  332. rerender(
  333. <ApiSource {...defaultProps} query="foo-t">
  334. {mock}
  335. </ApiSource>
  336. );
  337. await waitFor(() => {
  338. // Still have 4 results, but is re-ordered
  339. expect(mock.mock.calls[0][0].results).toHaveLength(6);
  340. expect(mock.mock.calls[0][0].results[0].item.model.slug).toBe('foo-team');
  341. });
  342. });
  343. describe('API queries', function () {
  344. it('calls API based on query string', function () {
  345. const {rerender} = render(<ApiSource {...defaultProps} query="" />);
  346. expect(projectsMock).toHaveBeenCalledTimes(1);
  347. rerender(<ApiSource {...defaultProps} query="f" />);
  348. // calls API when query string length is 1 char
  349. expect(projectsMock).toHaveBeenCalledTimes(2);
  350. rerender(<ApiSource {...defaultProps} query="fo" />);
  351. // calls API when query string length increases from 1 -> 2
  352. expect(projectsMock).toHaveBeenCalledTimes(3);
  353. rerender(<ApiSource {...defaultProps} query="foo" />);
  354. // Should not query API when query is > 2 chars
  355. expect(projectsMock).toHaveBeenCalledTimes(3);
  356. // re-queries API if first 2 characters are different
  357. rerender(<ApiSource {...defaultProps} query="ba" />);
  358. expect(projectsMock).toHaveBeenCalledTimes(4);
  359. // Does not requery when query stays the same
  360. rerender(<ApiSource {...defaultProps} query="ba" />);
  361. expect(projectsMock).toHaveBeenCalledTimes(4);
  362. // queries if we go from 2 chars -> 1 char
  363. rerender(<ApiSource {...defaultProps} query="b" />);
  364. expect(projectsMock).toHaveBeenCalledTimes(5);
  365. });
  366. });
  367. });