releaseSeries.spec.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import {Fragment} from 'react';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {RouterFixture} from 'sentry-fixture/routerFixture';
  4. import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import type {ReleaseSeriesProps} from 'sentry/components/charts/releaseSeries';
  6. import ReleaseSeries from 'sentry/components/charts/releaseSeries';
  7. import {lightTheme} from 'sentry/utils/theme';
  8. describe('ReleaseSeries', function () {
  9. const renderFunc = jest.fn(() => null);
  10. const organization = OrganizationFixture();
  11. let releases;
  12. let releasesMock;
  13. beforeEach(function () {
  14. jest.resetAllMocks();
  15. releases = [
  16. {
  17. version: 'sentry-android-shop@1.2.0',
  18. date: '2020-03-23T00:00:00Z',
  19. },
  20. ];
  21. MockApiClient.clearMockResponses();
  22. releasesMock = MockApiClient.addMockResponse({
  23. url: `/organizations/${organization.slug}/releases/stats/`,
  24. body: releases,
  25. });
  26. });
  27. const router = RouterFixture();
  28. const baseSeriesProps: ReleaseSeriesProps = {
  29. api: new MockApiClient(),
  30. organization: OrganizationFixture(),
  31. period: '14d',
  32. start: null,
  33. end: null,
  34. utc: false,
  35. projects: [],
  36. query: '',
  37. environments: [],
  38. children: renderFunc,
  39. params: router.params,
  40. routes: router.routes,
  41. router,
  42. location: router.location,
  43. theme: lightTheme,
  44. };
  45. it('does not fetch releases if releases is truthy', function () {
  46. render(
  47. <ReleaseSeries {...baseSeriesProps} organization={organization} releases={[]}>
  48. {renderFunc}
  49. </ReleaseSeries>
  50. );
  51. expect(releasesMock).not.toHaveBeenCalled();
  52. });
  53. it('fetches releases if no releases passed through props', async function () {
  54. render(<ReleaseSeries {...baseSeriesProps}>{renderFunc}</ReleaseSeries>);
  55. expect(releasesMock).toHaveBeenCalled();
  56. await waitFor(() =>
  57. expect(renderFunc).toHaveBeenCalledWith(
  58. expect.objectContaining({
  59. releases,
  60. })
  61. )
  62. );
  63. });
  64. it('fetches releases with project conditions', async function () {
  65. render(
  66. <ReleaseSeries {...baseSeriesProps} projects={[1, 2]}>
  67. {renderFunc}
  68. </ReleaseSeries>
  69. );
  70. await waitFor(() =>
  71. expect(releasesMock).toHaveBeenCalledWith(
  72. expect.anything(),
  73. expect.objectContaining({
  74. query: expect.objectContaining({project: [1, 2]}),
  75. })
  76. )
  77. );
  78. });
  79. it('fetches releases with environment conditions', async function () {
  80. render(
  81. <ReleaseSeries {...baseSeriesProps} environments={['dev', 'test']}>
  82. {renderFunc}
  83. </ReleaseSeries>
  84. );
  85. await waitFor(() =>
  86. expect(releasesMock).toHaveBeenCalledWith(
  87. expect.anything(),
  88. expect.objectContaining({
  89. query: expect.objectContaining({environment: ['dev', 'test']}),
  90. })
  91. )
  92. );
  93. });
  94. it('fetches releases with start and end date strings', async function () {
  95. render(
  96. <ReleaseSeries {...baseSeriesProps} start="2020-01-01" end="2020-01-31">
  97. {renderFunc}
  98. </ReleaseSeries>
  99. );
  100. await waitFor(() =>
  101. expect(releasesMock).toHaveBeenCalledWith(
  102. expect.anything(),
  103. expect.objectContaining({
  104. query: expect.objectContaining({
  105. start: '2020-01-01T00:00:00',
  106. end: '2020-01-31T00:00:00',
  107. }),
  108. })
  109. )
  110. );
  111. });
  112. it('fetches releases with start and end dates', async function () {
  113. const start = new Date(Date.UTC(2020, 0, 1, 12, 13, 14));
  114. const end = new Date(Date.UTC(2020, 0, 31, 14, 15, 16));
  115. render(
  116. <ReleaseSeries {...baseSeriesProps} start={start} end={end}>
  117. {renderFunc}
  118. </ReleaseSeries>
  119. );
  120. await waitFor(() =>
  121. expect(releasesMock).toHaveBeenCalledWith(
  122. expect.anything(),
  123. expect.objectContaining({
  124. query: expect.objectContaining({
  125. start: '2020-01-01T12:13:14',
  126. end: '2020-01-31T14:15:16',
  127. }),
  128. })
  129. )
  130. );
  131. });
  132. it('fetches releases with period', async function () {
  133. render(
  134. <ReleaseSeries {...baseSeriesProps} period="14d">
  135. {renderFunc}
  136. </ReleaseSeries>
  137. );
  138. await waitFor(() =>
  139. expect(releasesMock).toHaveBeenCalledWith(
  140. expect.anything(),
  141. expect.objectContaining({
  142. query: expect.objectContaining({statsPeriod: '14d'}),
  143. })
  144. )
  145. );
  146. });
  147. it('fetches on property updates', async function () {
  148. const wrapper = render(
  149. <ReleaseSeries {...baseSeriesProps} period="14d">
  150. {renderFunc}
  151. </ReleaseSeries>
  152. );
  153. const cases = [
  154. {period: '7d'},
  155. {start: '2020-01-01', end: '2020-01-02'},
  156. {projects: [1]},
  157. ];
  158. for (const scenario of cases) {
  159. releasesMock.mockReset();
  160. wrapper.rerender(
  161. <ReleaseSeries {...baseSeriesProps} {...scenario}>
  162. {renderFunc}
  163. </ReleaseSeries>
  164. );
  165. expect(releasesMock).toHaveBeenCalled();
  166. }
  167. await waitFor(() => expect(releasesMock).toHaveBeenCalledTimes(1));
  168. });
  169. it('doesnt not refetch releases with memoize enabled', async function () {
  170. const originalPeriod = '14d';
  171. const updatedPeriod = '7d';
  172. const wrapper = render(
  173. <ReleaseSeries {...baseSeriesProps} period={originalPeriod} memoized>
  174. {renderFunc}
  175. </ReleaseSeries>
  176. );
  177. await waitFor(() => expect(releasesMock).toHaveBeenCalledTimes(1));
  178. wrapper.rerender(
  179. <ReleaseSeries {...baseSeriesProps} period={updatedPeriod} memoized>
  180. {renderFunc}
  181. </ReleaseSeries>
  182. );
  183. await waitFor(() => expect(releasesMock).toHaveBeenCalledTimes(2));
  184. wrapper.rerender(
  185. <ReleaseSeries {...baseSeriesProps} period={originalPeriod} memoized>
  186. {renderFunc}
  187. </ReleaseSeries>
  188. );
  189. await waitFor(() => expect(releasesMock).toHaveBeenCalledTimes(2));
  190. });
  191. it('shares release fetches between components with memoize enabled', async function () {
  192. render(
  193. <Fragment>
  194. <ReleaseSeries {...baseSeriesProps} period="42d" memoized>
  195. {({releaseSeries}) => {
  196. return releaseSeries.length > 0 ? <span>Series 1</span> : null;
  197. }}
  198. </ReleaseSeries>
  199. <ReleaseSeries {...baseSeriesProps} period="42d" memoized>
  200. {({releaseSeries}) => {
  201. return releaseSeries.length > 0 ? <span>Series 2</span> : null;
  202. }}
  203. </ReleaseSeries>
  204. </Fragment>
  205. );
  206. await screen.findByText('Series 1');
  207. await screen.findByText('Series 2');
  208. await waitFor(() => expect(releasesMock).toHaveBeenCalledTimes(1));
  209. });
  210. it('generates an eCharts `markLine` series from releases', async function () {
  211. render(<ReleaseSeries {...baseSeriesProps}>{renderFunc}</ReleaseSeries>);
  212. await waitFor(() =>
  213. expect(renderFunc).toHaveBeenCalledWith(
  214. expect.objectContaining({
  215. releaseSeries: [
  216. expect.objectContaining({
  217. // we don't care about the other properties for now
  218. markLine: expect.objectContaining({
  219. data: [
  220. expect.objectContaining({
  221. name: '1.2.0, sentry-android-shop',
  222. value: '1.2.0, sentry-android-shop',
  223. xAxis: 1584921600000,
  224. }),
  225. ],
  226. }),
  227. }),
  228. ],
  229. })
  230. )
  231. );
  232. });
  233. it('allows updating the emphasized release', async function () {
  234. releases.push({
  235. version: 'sentry-android-shop@1.2.1',
  236. date: '2020-03-24T00:00:00Z',
  237. });
  238. const wrapper = render(
  239. <ReleaseSeries
  240. {...baseSeriesProps}
  241. emphasizeReleases={['sentry-android-shop@1.2.0']}
  242. >
  243. {renderFunc}
  244. </ReleaseSeries>
  245. );
  246. await waitFor(() =>
  247. expect(renderFunc).toHaveBeenCalledWith(
  248. expect.objectContaining({
  249. releaseSeries: [
  250. expect.objectContaining({
  251. // we don't care about the other properties for now
  252. markLine: expect.objectContaining({
  253. // the unemphasized releases have opacity 0.3
  254. lineStyle: expect.objectContaining({opacity: 0.3}),
  255. data: [
  256. expect.objectContaining({
  257. name: '1.2.1, sentry-android-shop',
  258. value: '1.2.1, sentry-android-shop',
  259. xAxis: 1585008000000,
  260. }),
  261. ],
  262. }),
  263. }),
  264. expect.objectContaining({
  265. // we don't care about the other properties for now
  266. markLine: expect.objectContaining({
  267. // the emphasized releases have opacity 0.8
  268. lineStyle: expect.objectContaining({opacity: 0.8}),
  269. data: [
  270. expect.objectContaining({
  271. name: '1.2.0, sentry-android-shop',
  272. value: '1.2.0, sentry-android-shop',
  273. xAxis: 1584921600000,
  274. }),
  275. ],
  276. }),
  277. }),
  278. ],
  279. })
  280. )
  281. );
  282. wrapper.rerender(
  283. <ReleaseSeries
  284. {...baseSeriesProps}
  285. emphasizeReleases={['sentry-android-shop@1.2.1']}
  286. >
  287. {renderFunc}
  288. </ReleaseSeries>
  289. );
  290. expect(renderFunc).toHaveBeenCalledWith(
  291. expect.objectContaining({
  292. releaseSeries: [
  293. expect.objectContaining({
  294. // we don't care about the other properties for now
  295. markLine: expect.objectContaining({
  296. // the unemphasized releases have opacity 0.3
  297. lineStyle: expect.objectContaining({opacity: 0.3}),
  298. data: [
  299. expect.objectContaining({
  300. name: '1.2.1, sentry-android-shop',
  301. value: '1.2.1, sentry-android-shop',
  302. xAxis: 1585008000000,
  303. }),
  304. ],
  305. }),
  306. }),
  307. expect.objectContaining({
  308. // we don't care about the other properties for now
  309. markLine: expect.objectContaining({
  310. // the emphasized releases have opacity 0.8
  311. lineStyle: expect.objectContaining({opacity: 0.8}),
  312. data: [
  313. expect.objectContaining({
  314. name: '1.2.0, sentry-android-shop',
  315. value: '1.2.0, sentry-android-shop',
  316. xAxis: 1584921600000,
  317. }),
  318. ],
  319. }),
  320. }),
  321. ],
  322. })
  323. );
  324. });
  325. });