issueCronCheckTimeline.spec.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {EventFixture} from 'sentry-fixture/event';
  2. import {GroupFixture} from 'sentry-fixture/group';
  3. import {OrganizationFixture} from 'sentry-fixture/organization';
  4. import {ProjectFixture} from 'sentry-fixture/project';
  5. import {render, screen, within} from 'sentry-test/reactTestingLibrary';
  6. import {useTimeWindowConfig} from 'sentry/components/checkInTimeline/hooks/useTimeWindowConfig';
  7. import type {StatsBucket} from 'sentry/components/checkInTimeline/types';
  8. import {getConfigFromTimeRange} from 'sentry/components/checkInTimeline/utils/getConfigFromTimeRange';
  9. import GroupStore from 'sentry/stores/groupStore';
  10. import ProjectsStore from 'sentry/stores/projectsStore';
  11. import {IssueCategory, IssueType} from 'sentry/types/group';
  12. import {IssueCronCheckTimeline} from 'sentry/views/issueDetails/streamline/issueCronCheckTimeline';
  13. import {CheckInStatus} from 'sentry/views/monitors/types';
  14. import {statusToText} from 'sentry/views/monitors/utils';
  15. const startTime = new Date('2025-01-01T11:00:00Z');
  16. jest.mock('sentry/components/checkInTimeline/hooks/useTimeWindowConfig');
  17. jest
  18. .mocked(useTimeWindowConfig)
  19. .mockReturnValue(
  20. getConfigFromTimeRange(
  21. startTime,
  22. new Date(startTime.getTime() + 1000 * 60 * 60),
  23. 1000
  24. )
  25. );
  26. const mockBucket: StatsBucket<CheckInStatus> = {
  27. [CheckInStatus.OK]: 1,
  28. [CheckInStatus.ERROR]: 1,
  29. [CheckInStatus.IN_PROGRESS]: 1,
  30. [CheckInStatus.MISSED]: 1,
  31. [CheckInStatus.TIMEOUT]: 1,
  32. [CheckInStatus.UNKNOWN]: 1,
  33. };
  34. describe('IssueCronCheckTimeline', () => {
  35. const cronAlertId = '123';
  36. const organization = OrganizationFixture();
  37. const project = ProjectFixture({
  38. environments: ['dev', 'prod'],
  39. });
  40. const group = GroupFixture({
  41. issueCategory: IssueCategory.CRON,
  42. issueType: IssueType.MONITOR_CHECK_IN_FAILURE,
  43. });
  44. const event = EventFixture({
  45. tags: [
  46. {
  47. key: 'monitor.id',
  48. value: cronAlertId,
  49. },
  50. ],
  51. });
  52. beforeEach(() => {
  53. GroupStore.init();
  54. GroupStore.add([group]);
  55. ProjectsStore.init();
  56. ProjectsStore.loadInitialData([project]);
  57. MockApiClient.clearMockResponses();
  58. MockApiClient.addMockResponse({
  59. url: `/organizations/${organization.slug}/issues/${group.id}/`,
  60. body: group,
  61. });
  62. MockApiClient.addMockResponse({
  63. url: `/organizations/${organization.slug}/issues/${group.id}/events/recommended/`,
  64. body: event,
  65. });
  66. });
  67. it('renders the cron check timeline with a legend and data', async function () {
  68. MockApiClient.addMockResponse({
  69. url: `/organizations/${organization.slug}/monitors-stats/`,
  70. query: {
  71. monitor: [cronAlertId],
  72. project: project.slug,
  73. environment: 'dev',
  74. },
  75. body: {
  76. [cronAlertId]: [[startTime.getTime() / 1000, {dev: mockBucket}]],
  77. },
  78. });
  79. render(<IssueCronCheckTimeline group={group} />, {organization});
  80. expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
  81. const legend = screen.getByRole('caption');
  82. expect(within(legend).getByText(statusToText[CheckInStatus.OK])).toBeInTheDocument();
  83. expect(screen.getByRole('figure')).toBeInTheDocument();
  84. const gridlineLabels = [
  85. 'Jan 1, 2025 11:00 AM UTC',
  86. '11:10 AM',
  87. '11:20 AM',
  88. '11:30 AM',
  89. '11:40 AM',
  90. '11:50 AM',
  91. ];
  92. gridlineLabels.forEach(label => {
  93. expect(screen.getByText(label)).toBeInTheDocument();
  94. });
  95. // Do not show environment labels if there is only one environment
  96. expect(screen.queryByText('dev')).not.toBeInTheDocument();
  97. });
  98. it('hides statuses from legend if not present in data', async function () {
  99. const newBucket = {
  100. ...mockBucket,
  101. // OK is always shown, even with no data
  102. [CheckInStatus.OK]: 0,
  103. [CheckInStatus.ERROR]: 0,
  104. [CheckInStatus.IN_PROGRESS]: 0,
  105. [CheckInStatus.TIMEOUT]: 0,
  106. };
  107. MockApiClient.addMockResponse({
  108. url: `/organizations/${organization.slug}/monitors-stats/`,
  109. query: {
  110. monitor: [cronAlertId],
  111. project: project.slug,
  112. environment: 'dev',
  113. },
  114. body: {
  115. [cronAlertId]: [[startTime.getTime() / 1000, {dev: newBucket}]],
  116. },
  117. });
  118. render(<IssueCronCheckTimeline group={group} />, {organization});
  119. expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
  120. const legend = screen.getByRole('caption');
  121. [
  122. statusToText[CheckInStatus.OK],
  123. statusToText[CheckInStatus.MISSED],
  124. statusToText[CheckInStatus.UNKNOWN],
  125. ].forEach(status => {
  126. expect(within(legend).getByText(status)).toBeInTheDocument();
  127. });
  128. [
  129. statusToText[CheckInStatus.ERROR],
  130. statusToText[CheckInStatus.IN_PROGRESS],
  131. statusToText[CheckInStatus.TIMEOUT],
  132. ].forEach(status => {
  133. expect(within(legend).queryByText(status)).not.toBeInTheDocument();
  134. });
  135. });
  136. it('displays multiple environment legends and labels', async function () {
  137. const envBucketMapping = {
  138. dev: {
  139. [CheckInStatus.OK]: 1,
  140. [CheckInStatus.ERROR]: 1,
  141. [CheckInStatus.IN_PROGRESS]: 0,
  142. [CheckInStatus.MISSED]: 0,
  143. [CheckInStatus.TIMEOUT]: 1,
  144. [CheckInStatus.UNKNOWN]: 0,
  145. },
  146. prod: {
  147. [CheckInStatus.OK]: 0,
  148. [CheckInStatus.ERROR]: 0,
  149. [CheckInStatus.IN_PROGRESS]: 1,
  150. [CheckInStatus.MISSED]: 1,
  151. [CheckInStatus.TIMEOUT]: 0,
  152. [CheckInStatus.UNKNOWN]: 1,
  153. },
  154. };
  155. MockApiClient.addMockResponse({
  156. url: `/organizations/${organization.slug}/monitors-stats/`,
  157. query: {
  158. monitor: [cronAlertId],
  159. project: project.slug,
  160. environment: [],
  161. },
  162. body: {
  163. [cronAlertId]: [[startTime.getTime() / 1000, envBucketMapping]],
  164. },
  165. });
  166. render(<IssueCronCheckTimeline group={group} />, {organization});
  167. expect(await screen.findByTestId('check-in-placeholder')).not.toBeInTheDocument();
  168. const legend = screen.getByRole('caption');
  169. // All statuses from both environment timelines should be present
  170. [
  171. statusToText[CheckInStatus.OK],
  172. statusToText[CheckInStatus.ERROR],
  173. statusToText[CheckInStatus.IN_PROGRESS],
  174. statusToText[CheckInStatus.MISSED],
  175. statusToText[CheckInStatus.TIMEOUT],
  176. statusToText[CheckInStatus.UNKNOWN],
  177. ].forEach(status => {
  178. expect(within(legend).getByText(status)).toBeInTheDocument();
  179. });
  180. // Environment labels should now be present
  181. expect(screen.getByText('dev')).toBeInTheDocument();
  182. expect(screen.getByText('prod')).toBeInTheDocument();
  183. });
  184. });