useInitialTimeOffsetMs.spec.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {reactHooks} from 'sentry-test/reactTestingLibrary';
  3. import fetchReplayClicks from 'sentry/utils/replays/fetchReplayClicks';
  4. import useInitialTimeOffsetMs from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
  5. import {useLocation} from 'sentry/utils/useLocation';
  6. jest.mock('sentry/utils/useLocation');
  7. jest.mock('sentry/utils/replays/fetchReplayClicks');
  8. const MockUseLocation = useLocation as jest.MockedFunction<typeof useLocation>;
  9. const MockFetchReplayClicks = fetchReplayClicks as jest.MockedFunction<
  10. typeof fetchReplayClicks
  11. >;
  12. const {organization, project} = initializeOrg();
  13. const replay = TestStubs.ReplayRecord();
  14. const NOON = '2023-04-14T12:00:00';
  15. const FIVE_PAST_NOON = '2023-04-14T12:05:00';
  16. function mockQuery(query: Record<string, string>) {
  17. MockUseLocation.mockReturnValue({
  18. pathname: '',
  19. search: '',
  20. query,
  21. hash: '',
  22. state: undefined,
  23. action: 'PUSH',
  24. key: '',
  25. });
  26. }
  27. describe('useInitialTimeOffsetMs', () => {
  28. beforeEach(() => {
  29. MockUseLocation.mockClear();
  30. MockFetchReplayClicks.mockClear();
  31. });
  32. describe('fromOffset', () => {
  33. it('should return an offset, in ms, if `t` exists in the query', async () => {
  34. const offsetInSeconds = 23;
  35. mockQuery({t: String(offsetInSeconds)});
  36. const {result, waitForNextUpdate} = reactHooks.renderHook(useInitialTimeOffsetMs, {
  37. initialProps: {
  38. orgSlug: organization.slug,
  39. projectSlug: project.slug,
  40. replayId: replay.id,
  41. replayStartTimestampMs: undefined,
  42. },
  43. });
  44. await waitForNextUpdate();
  45. expect(result.current).toStrictEqual({offsetMs: 23 * 1000});
  46. });
  47. it('should prefer reading `t` over the other qs params', async () => {
  48. const offsetInSeconds = 23;
  49. mockQuery({
  50. t: String(offsetInSeconds),
  51. event_t: FIVE_PAST_NOON,
  52. query: 'click.tag:button',
  53. });
  54. const {result, waitForNextUpdate} = reactHooks.renderHook(useInitialTimeOffsetMs, {
  55. initialProps: {
  56. orgSlug: organization.slug,
  57. projectSlug: project.slug,
  58. replayId: replay.id,
  59. replayStartTimestampMs: undefined,
  60. },
  61. });
  62. await waitForNextUpdate();
  63. expect(result.current).toStrictEqual({offsetMs: 23 * 1000});
  64. expect(MockFetchReplayClicks).toHaveBeenCalledTimes(0);
  65. });
  66. });
  67. describe('fromEventTimestamp', () => {
  68. it('should calculate the difference between an event timestamp and the replay start timestamp', async () => {
  69. const noon = '2023-04-14T12:00:00';
  70. const fivePastNoon = '2023-04-14T12:05:00';
  71. mockQuery({event_t: fivePastNoon});
  72. const {result, waitForNextUpdate} = reactHooks.renderHook(useInitialTimeOffsetMs, {
  73. initialProps: {
  74. orgSlug: organization.slug,
  75. projectSlug: project.slug,
  76. replayId: replay.id,
  77. replayStartTimestampMs: new Date(noon).getTime(),
  78. },
  79. });
  80. await waitForNextUpdate();
  81. // Expecting 5 minutes difference, in ms
  82. expect(result.current).toStrictEqual({offsetMs: 5 * 60 * 1000});
  83. });
  84. it('should return 0 offset if there is no replayStartTimetsamp, then recalculate when the startTimestamp appears', async () => {
  85. mockQuery({event_t: FIVE_PAST_NOON});
  86. const {result, rerender, waitForNextUpdate} = reactHooks.renderHook(
  87. useInitialTimeOffsetMs,
  88. {
  89. initialProps: {
  90. orgSlug: organization.slug,
  91. projectSlug: project.slug,
  92. replayId: replay.id,
  93. replayStartTimestampMs: undefined,
  94. },
  95. }
  96. );
  97. await waitForNextUpdate();
  98. expect(result.current).toStrictEqual({offsetMs: 0});
  99. rerender({
  100. orgSlug: organization.slug,
  101. projectSlug: project.slug,
  102. replayId: replay.id,
  103. replayStartTimestampMs: new Date(NOON).getTime(),
  104. });
  105. await waitForNextUpdate();
  106. // Expecting 5 minutes difference, in ms
  107. expect(result.current).toStrictEqual({offsetMs: 5 * 60 * 1000});
  108. });
  109. it('should prefer reading `event_t` over the other search query params', async () => {
  110. mockQuery({
  111. event_t: FIVE_PAST_NOON,
  112. query: 'click.tag:button',
  113. });
  114. MockFetchReplayClicks.mockResolvedValue({
  115. fetchError: undefined,
  116. pageLinks: '',
  117. clicks: [],
  118. });
  119. const {result, waitForNextUpdate} = reactHooks.renderHook(useInitialTimeOffsetMs, {
  120. initialProps: {
  121. orgSlug: organization.slug,
  122. projectSlug: project.slug,
  123. replayId: replay.id,
  124. replayStartTimestampMs: new Date(NOON).getTime(),
  125. },
  126. });
  127. await waitForNextUpdate();
  128. expect(result.current).toStrictEqual({offsetMs: 5 * 60 * 1000});
  129. expect(MockFetchReplayClicks).toHaveBeenCalledTimes(0);
  130. });
  131. });
  132. describe('fromListPageQuery', () => {
  133. it('should skip this strategy if there is no `click.*` term in the query', async () => {
  134. mockQuery({query: 'user.email:*@sentry.io'});
  135. const {result, waitForNextUpdate} = reactHooks.renderHook(useInitialTimeOffsetMs, {
  136. initialProps: {
  137. orgSlug: organization.slug,
  138. projectSlug: project.slug,
  139. replayId: replay.id,
  140. replayStartTimestampMs: new Date(NOON).getTime(),
  141. },
  142. });
  143. await waitForNextUpdate();
  144. expect(MockFetchReplayClicks).toHaveBeenCalledTimes(0);
  145. expect(result.current).toStrictEqual({offsetMs: 0});
  146. });
  147. it('should request a list of click results, and calculate the offset from the first result', async () => {
  148. mockQuery({query: 'click.tag:button'});
  149. MockFetchReplayClicks.mockResolvedValue({
  150. fetchError: undefined,
  151. pageLinks: '',
  152. clicks: [{node_id: 7, timestamp: FIVE_PAST_NOON}],
  153. });
  154. const {result, waitForNextUpdate} = reactHooks.renderHook(useInitialTimeOffsetMs, {
  155. initialProps: {
  156. orgSlug: organization.slug,
  157. projectSlug: project.slug,
  158. replayId: replay.id,
  159. replayStartTimestampMs: new Date(NOON).getTime(),
  160. },
  161. });
  162. await waitForNextUpdate();
  163. expect(MockFetchReplayClicks).toHaveBeenCalledTimes(1);
  164. // Expecting 5 minutes difference, in ms
  165. expect(result.current).toStrictEqual({
  166. highlight: {
  167. annotation: 'click.tag:button',
  168. nodeId: 7,
  169. spotlight: true,
  170. },
  171. offsetMs: 5 * 60 * 1000,
  172. });
  173. });
  174. it('should not call call fetch twice when props change', async () => {
  175. mockQuery({query: 'click.tag:button'});
  176. MockFetchReplayClicks.mockResolvedValue({
  177. fetchError: undefined,
  178. pageLinks: '',
  179. clicks: [{node_id: 7, timestamp: FIVE_PAST_NOON}],
  180. });
  181. const {result, rerender, waitForNextUpdate} = reactHooks.renderHook(
  182. useInitialTimeOffsetMs,
  183. {
  184. initialProps: {
  185. orgSlug: organization.slug,
  186. projectSlug: project.slug,
  187. replayId: replay.id,
  188. replayStartTimestampMs: undefined,
  189. },
  190. }
  191. );
  192. await waitForNextUpdate();
  193. expect(MockFetchReplayClicks).toHaveBeenCalledTimes(0);
  194. expect(result.current).toStrictEqual({
  195. offsetMs: 0,
  196. });
  197. rerender({
  198. orgSlug: organization.slug,
  199. projectSlug: project.slug,
  200. replayId: replay.id,
  201. replayStartTimestampMs: new Date(NOON).getTime(),
  202. });
  203. await waitForNextUpdate();
  204. expect(MockFetchReplayClicks).toHaveBeenCalledTimes(1);
  205. expect(result.current).toStrictEqual({
  206. highlight: {
  207. annotation: 'click.tag:button',
  208. nodeId: 7,
  209. spotlight: true,
  210. },
  211. offsetMs: 5 * 60 * 1000,
  212. });
  213. });
  214. });
  215. });