index.spec.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import ProjectsStore from 'app/stores/projectsStore';
  4. import ReleasesList from 'app/views/releases/list/';
  5. import {DisplayOption, SortOption, StatusOption} from 'app/views/releases/list/utils';
  6. describe('ReleasesList', function () {
  7. const {organization, routerContext, router} = initializeOrg();
  8. const props = {
  9. router,
  10. organization,
  11. selection: {
  12. projects: [],
  13. datetime: {
  14. period: '14d',
  15. },
  16. },
  17. params: {orgId: organization.slug},
  18. location: {
  19. query: {
  20. query: 'derp',
  21. sort: SortOption.SESSIONS,
  22. healthStatsPeriod: '24h',
  23. somethingBad: 'XXX',
  24. status: StatusOption.ACTIVE,
  25. },
  26. },
  27. };
  28. let wrapper, endpointMock, sessionApiMock;
  29. beforeEach(async function () {
  30. ProjectsStore.loadInitialData(organization.projects);
  31. endpointMock = MockApiClient.addMockResponse({
  32. url: '/organizations/org-slug/releases/',
  33. body: [
  34. TestStubs.Release({version: '1.0.0'}),
  35. TestStubs.Release({version: '1.0.1'}),
  36. {
  37. ...TestStubs.Release({version: 'af4f231ec9a8'}),
  38. projects: [
  39. {
  40. id: 4383604,
  41. name: 'Sentry-IOS-Shop',
  42. slug: 'sentry-ios-shop',
  43. hasHealthData: false,
  44. },
  45. ],
  46. },
  47. ],
  48. });
  49. sessionApiMock = MockApiClient.addMockResponse({
  50. url: `/organizations/org-slug/sessions/`,
  51. body: null,
  52. });
  53. MockApiClient.addMockResponse({
  54. url: '/organizations/org-slug/projects/',
  55. body: [],
  56. });
  57. wrapper = mountWithTheme(<ReleasesList {...props} />, routerContext);
  58. await tick();
  59. wrapper.update();
  60. });
  61. afterEach(function () {
  62. ProjectsStore.reset();
  63. MockApiClient.clearMockResponses();
  64. });
  65. it('renders list', function () {
  66. const items = wrapper.find('StyledPanel');
  67. expect(items).toHaveLength(3);
  68. expect(items.at(0).text()).toContain('1.0.0');
  69. expect(items.at(0).text()).toContain('Adoption');
  70. expect(items.at(1).text()).toContain('1.0.1');
  71. expect(items.at(1).find('CountColumn').at(1).text()).toContain('\u2014');
  72. expect(items.at(2).text()).toContain('af4f231ec9a8');
  73. expect(items.at(2).find('Header').text()).toContain('Project');
  74. });
  75. it('displays the right empty state', function () {
  76. let location;
  77. MockApiClient.addMockResponse({
  78. url: '/organizations/org-slug/releases/',
  79. body: [],
  80. });
  81. location = {query: {}};
  82. wrapper = mountWithTheme(
  83. <ReleasesList {...props} location={location} />,
  84. routerContext
  85. );
  86. expect(wrapper.find('StyledPanel')).toHaveLength(0);
  87. expect(wrapper.find('ReleasePromo').text()).toContain('Demystify Releases');
  88. location = {query: {statsPeriod: '30d'}};
  89. wrapper = mountWithTheme(
  90. <ReleasesList {...props} location={location} />,
  91. routerContext
  92. );
  93. expect(wrapper.find('StyledPanel')).toHaveLength(0);
  94. expect(wrapper.find('ReleasePromo').text()).toContain('Demystify Releases');
  95. location = {query: {query: 'abc'}};
  96. wrapper = mountWithTheme(
  97. <ReleasesList {...props} location={location} />,
  98. routerContext
  99. );
  100. expect(wrapper.find('EmptyMessage').text()).toEqual(
  101. "There are no releases that match: 'abc'."
  102. );
  103. location = {query: {sort: SortOption.SESSIONS, statsPeriod: '7d'}};
  104. wrapper = mountWithTheme(
  105. <ReleasesList {...props} location={location} />,
  106. routerContext
  107. );
  108. expect(wrapper.find('EmptyMessage').text()).toEqual(
  109. 'There are no releases with data in the last 7 days.'
  110. );
  111. location = {query: {sort: SortOption.USERS_24_HOURS, statsPeriod: '7d'}};
  112. wrapper = mountWithTheme(
  113. <ReleasesList {...props} location={location} />,
  114. routerContext
  115. );
  116. expect(wrapper.find('EmptyMessage').text()).toEqual(
  117. 'There are no releases with active user data (users in the last 24 hours).'
  118. );
  119. });
  120. it('searches for a release', function () {
  121. const input = wrapper.find('input');
  122. expect(endpointMock).toHaveBeenCalledWith(
  123. '/organizations/org-slug/releases/',
  124. expect.objectContaining({
  125. query: expect.objectContaining({query: 'derp'}),
  126. })
  127. );
  128. expect(input.prop('value')).toBe('derp');
  129. input.simulate('change', {target: {value: 'a'}}).simulate('submit');
  130. expect(router.push).toHaveBeenCalledWith({
  131. query: expect.objectContaining({query: 'a'}),
  132. });
  133. });
  134. it('sorts releases', function () {
  135. expect(endpointMock).toHaveBeenCalledWith(
  136. '/organizations/org-slug/releases/',
  137. expect.objectContaining({
  138. query: expect.objectContaining({
  139. sort: SortOption.SESSIONS,
  140. }),
  141. })
  142. );
  143. const sortDropdown = wrapper.find('ReleaseListSortOptions');
  144. const sortByOptions = sortDropdown.find('DropdownItem span');
  145. const dateCreatedOption = sortByOptions.at(0);
  146. expect(sortByOptions).toHaveLength(5);
  147. expect(dateCreatedOption.text()).toEqual('Date Created');
  148. const healthStatsControls = wrapper.find('CountColumn span').first();
  149. expect(healthStatsControls.text()).toEqual('Count');
  150. dateCreatedOption.simulate('click');
  151. expect(router.push).toHaveBeenCalledWith({
  152. query: expect.objectContaining({
  153. sort: SortOption.DATE,
  154. }),
  155. });
  156. });
  157. it('display the right Crash Free column', async function () {
  158. const displayDropdown = wrapper.find('ReleaseListDisplayOptions');
  159. const activeDisplay = displayDropdown.find('DropdownButton button');
  160. expect(activeDisplay.text()).toEqual('DisplaySessions');
  161. const displayOptions = displayDropdown.find('DropdownItem');
  162. expect(displayOptions).toHaveLength(2);
  163. const crashFreeSessionsOption = displayOptions.at(0);
  164. expect(crashFreeSessionsOption.props().isActive).toEqual(true);
  165. expect(crashFreeSessionsOption.text()).toEqual('Sessions');
  166. const crashFreeUsersOption = displayOptions.at(1);
  167. expect(crashFreeUsersOption.text()).toEqual('Users');
  168. expect(crashFreeUsersOption.props().isActive).toEqual(false);
  169. crashFreeUsersOption.find('span').simulate('click');
  170. expect(router.push).toHaveBeenCalledWith({
  171. query: expect.objectContaining({
  172. display: DisplayOption.USERS,
  173. }),
  174. });
  175. });
  176. it('displays archived releases', function () {
  177. const archivedWrapper = mountWithTheme(
  178. <ReleasesList {...props} location={{query: {status: StatusOption.ARCHIVED}}} />,
  179. routerContext
  180. );
  181. expect(endpointMock).toHaveBeenLastCalledWith(
  182. '/organizations/org-slug/releases/',
  183. expect.objectContaining({
  184. query: expect.objectContaining({status: StatusOption.ARCHIVED}),
  185. })
  186. );
  187. expect(archivedWrapper.find('ReleaseArchivedNotice').exists()).toBeTruthy();
  188. const statusOptions = archivedWrapper
  189. .find('ReleaseListStatusOptions')
  190. .first()
  191. .find('DropdownItem span');
  192. const statusActiveOption = statusOptions.at(0);
  193. const statusArchivedOption = statusOptions.at(1);
  194. expect(statusOptions).toHaveLength(2);
  195. expect(statusActiveOption.text()).toEqual('Active');
  196. expect(statusArchivedOption.text()).toEqual('Archived');
  197. statusActiveOption.simulate('click');
  198. expect(router.push).toHaveBeenLastCalledWith({
  199. query: expect.objectContaining({
  200. status: StatusOption.ACTIVE,
  201. }),
  202. });
  203. expect(wrapper.find('ReleaseArchivedNotice').exists()).toBeFalsy();
  204. statusArchivedOption.simulate('click');
  205. expect(router.push).toHaveBeenLastCalledWith({
  206. query: expect.objectContaining({
  207. status: StatusOption.ARCHIVED,
  208. }),
  209. });
  210. });
  211. it('calls api with only explicitly permitted query params', function () {
  212. expect(endpointMock).toHaveBeenCalledWith(
  213. '/organizations/org-slug/releases/',
  214. expect.objectContaining({
  215. query: expect.not.objectContaining({
  216. somethingBad: 'XXX',
  217. }),
  218. })
  219. );
  220. });
  221. it('calls session api for health data', async function () {
  222. expect(sessionApiMock).toHaveBeenCalledTimes(3);
  223. expect(sessionApiMock).toHaveBeenCalledWith(
  224. '/organizations/org-slug/sessions/',
  225. expect.objectContaining({
  226. query: expect.objectContaining({
  227. field: ['sum(session)'],
  228. groupBy: ['project', 'release', 'session.status'],
  229. interval: '1d',
  230. query: 'release:1.0.0 OR release:1.0.1 OR release:af4f231ec9a8',
  231. statsPeriod: '14d',
  232. }),
  233. })
  234. );
  235. expect(sessionApiMock).toHaveBeenCalledWith(
  236. '/organizations/org-slug/sessions/',
  237. expect.objectContaining({
  238. query: expect.objectContaining({
  239. field: ['sum(session)'],
  240. groupBy: ['project'],
  241. interval: '1h',
  242. query: undefined,
  243. statsPeriod: '24h',
  244. }),
  245. })
  246. );
  247. expect(sessionApiMock).toHaveBeenCalledWith(
  248. '/organizations/org-slug/sessions/',
  249. expect.objectContaining({
  250. query: expect.objectContaining({
  251. field: ['sum(session)'],
  252. groupBy: ['project', 'release'],
  253. interval: '1h',
  254. query: 'release:1.0.0 OR release:1.0.1 OR release:af4f231ec9a8',
  255. statsPeriod: '24h',
  256. }),
  257. })
  258. );
  259. });
  260. it('shows health rows only for selected projects in global header', function () {
  261. MockApiClient.addMockResponse({
  262. url: '/organizations/org-slug/releases/',
  263. body: [
  264. {
  265. ...TestStubs.Release({version: '2.0.0'}),
  266. projects: [
  267. {
  268. id: 1,
  269. name: 'Test',
  270. slug: 'test',
  271. },
  272. {
  273. id: 2,
  274. name: 'Test2',
  275. slug: 'test2',
  276. },
  277. {
  278. id: 3,
  279. name: 'Test3',
  280. slug: 'test3',
  281. },
  282. ],
  283. },
  284. ],
  285. });
  286. const healthSection = mountWithTheme(
  287. <ReleasesList {...props} selection={{projects: [2]}} />,
  288. routerContext
  289. ).find('ReleaseHealth');
  290. const hiddenProjectsMessage = healthSection.find('HiddenProjectsMessage');
  291. expect(hiddenProjectsMessage.text()).toBe('2 hidden projects');
  292. expect(hiddenProjectsMessage.find('Tooltip').prop('title')).toBe('test, test3');
  293. expect(healthSection.find('ProjectRow').length).toBe(1);
  294. expect(healthSection.find('ProjectBadge').text()).toBe('test2');
  295. });
  296. it('does not hide health rows when "All Projects" are selected in global header', function () {
  297. MockApiClient.addMockResponse({
  298. url: '/organizations/org-slug/releases/',
  299. body: [TestStubs.Release({version: '2.0.0'})],
  300. });
  301. const healthSection = mountWithTheme(
  302. <ReleasesList {...props} selection={{projects: [-1]}} />,
  303. routerContext
  304. ).find('ReleaseHealth');
  305. expect(healthSection.find('HiddenProjectsMessage').exists()).toBeFalsy();
  306. expect(healthSection.find('ProjectRow').length).toBe(1);
  307. });
  308. });