Skip to content

Commit b89db0f

Browse files
move from component to hook
Signed-off-by: Adhitya Mamallan <[email protected]>
1 parent 01e8c2d commit b89db0f

File tree

7 files changed

+300
-31
lines changed

7 files changed

+300
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import React from 'react';
2+
3+
import dayjs from 'dayjs';
4+
5+
import { render, screen, act } from '@/test-utils/rtl';
6+
7+
import { WorkflowExecutionCloseStatus } from '@/__generated__/proto-ts/uber/cadence/api/v1/WorkflowExecutionCloseStatus';
8+
9+
import getFormattedEventsDuration from '../helpers/get-formatted-events-duration';
10+
import WorkflowHistoryEventGroupDuration from '../workflow-history-event-group-duration';
11+
import type { Props } from '../workflow-history-event-group-duration.types';
12+
13+
jest.mock('../helpers/get-formatted-events-duration', () =>
14+
jest.fn((startTime, endTime) =>
15+
String(dayjs(endTime ?? undefined).diff(dayjs(startTime), 'seconds'))
16+
)
17+
);
18+
19+
const mockStartTime = new Date('2024-01-01T10:00:00Z').getTime();
20+
const mockCloseTime = new Date('2024-01-01T10:01:00Z').getTime();
21+
const mockNow = new Date('2024-01-01T10:02:00Z').getTime();
22+
23+
describe('WorkflowHistoryEventGroupDuration', () => {
24+
beforeEach(() => {
25+
jest.useFakeTimers();
26+
jest.setSystemTime(mockNow);
27+
});
28+
29+
afterEach(() => {
30+
jest.clearAllMocks();
31+
jest.useRealTimers();
32+
});
33+
34+
it('renders duration for completed event', () => {
35+
setup({
36+
closeTime: mockCloseTime,
37+
});
38+
39+
expect(screen.getByText('60')).toBeInTheDocument();
40+
});
41+
42+
it('renders duration for ongoing event', () => {
43+
setup({
44+
closeTime: null,
45+
});
46+
expect(screen.getByText('120')).toBeInTheDocument();
47+
});
48+
49+
it('does not render duration for single event', () => {
50+
setup({
51+
eventsCount: 1,
52+
hasMissingEvents: false,
53+
});
54+
55+
expect(screen.queryByText('60')).not.toBeInTheDocument();
56+
});
57+
58+
it('renders duration for single event with missing events', () => {
59+
setup({
60+
eventsCount: 1,
61+
hasMissingEvents: true,
62+
});
63+
64+
expect(screen.getByText('120')).toBeInTheDocument();
65+
});
66+
67+
it('does not render duration when loading more events', () => {
68+
setup({
69+
loadingMoreEvents: true,
70+
});
71+
72+
expect(screen.queryByText('60')).not.toBeInTheDocument();
73+
});
74+
75+
it('does not render duration when workflow is archived without close time', () => {
76+
setup({
77+
closeTime: null,
78+
workflowIsArchived: true,
79+
});
80+
81+
expect(screen.queryByText('120')).not.toBeInTheDocument();
82+
});
83+
84+
it('does not render duration when workflow has close status without close time', () => {
85+
setup({
86+
workflowCloseStatus:
87+
WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED,
88+
});
89+
90+
expect(screen.queryByText('60')).not.toBeInTheDocument();
91+
});
92+
93+
it('updates duration for ongoing event every second', () => {
94+
setup({
95+
closeTime: null,
96+
});
97+
expect(screen.getByText('120')).toBeInTheDocument();
98+
99+
(getFormattedEventsDuration as jest.Mock).mockClear();
100+
act(() => {
101+
jest.advanceTimersByTime(1000);
102+
jest.setSystemTime(new Date(mockNow + 1000));
103+
});
104+
expect(screen.getByText('121')).toBeInTheDocument();
105+
act(() => {
106+
jest.advanceTimersByTime(1000);
107+
jest.setSystemTime(new Date(mockNow + 2000));
108+
});
109+
expect(screen.getByText('122')).toBeInTheDocument();
110+
111+
expect(getFormattedEventsDuration).toHaveBeenCalledTimes(2);
112+
});
113+
114+
it('cleans up interval when component unmounts', () => {
115+
const { unmount } = setup();
116+
117+
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
118+
unmount();
119+
120+
expect(clearIntervalSpy).toHaveBeenCalled();
121+
});
122+
123+
it('uses workflow close time when close time is not provided', () => {
124+
setup({
125+
closeTime: null,
126+
workflowCloseTime: mockCloseTime,
127+
});
128+
129+
expect(getFormattedEventsDuration).toHaveBeenCalledWith(
130+
mockStartTime,
131+
mockCloseTime,
132+
expect.any(Boolean)
133+
);
134+
expect(screen.getByText('60')).toBeInTheDocument();
135+
});
136+
});
137+
138+
function setup({
139+
startTime = mockStartTime,
140+
closeTime,
141+
eventsCount = 2,
142+
hasMissingEvents = false,
143+
loadingMoreEvents = false,
144+
workflowIsArchived = false,
145+
workflowCloseStatus = WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID,
146+
workflowCloseTime = null,
147+
}: Partial<Props> = {}) {
148+
return render(
149+
<WorkflowHistoryEventGroupDuration
150+
startTime={startTime}
151+
closeTime={closeTime}
152+
eventsCount={eventsCount}
153+
hasMissingEvents={hasMissingEvents}
154+
loadingMoreEvents={loadingMoreEvents}
155+
workflowIsArchived={workflowIsArchived}
156+
workflowCloseStatus={workflowCloseStatus}
157+
workflowCloseTime={workflowCloseTime}
158+
/>
159+
);
160+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import getFormattedEventsDuration from '../get-formatted-events-duration';
2+
3+
jest.mock('@/utils/data-formatters/format-duration', () => ({
4+
__esModule: true,
5+
default: jest.fn(
6+
(duration, { minUnit }) =>
7+
`mocked: ${duration.seconds}s${minUnit === 'ms' ? ` ${duration.nanos / 1000000}ms` : ''}`
8+
),
9+
}));
10+
11+
const mockNow = new Date('2024-01-01T10:02:00Z');
12+
13+
describe('getFormattedEventsDuration', () => {
14+
beforeEach(() => {
15+
jest.useFakeTimers();
16+
jest.setSystemTime(mockNow);
17+
});
18+
19+
afterEach(() => {
20+
jest.useRealTimers();
21+
});
22+
23+
it('should return 0s for identical start and end times', () => {
24+
const duration = getFormattedEventsDuration(
25+
'2021-01-01T00:00:00Z',
26+
'2021-01-01T00:00:00Z'
27+
);
28+
expect(duration).toEqual('mocked: 0s 0ms');
29+
});
30+
31+
it('should return correct duration for 1 minute', () => {
32+
const duration = getFormattedEventsDuration(
33+
'2021-01-01T00:00:00Z',
34+
'2021-01-01T00:01:00Z'
35+
);
36+
expect(duration).toEqual('mocked: 60s 0ms');
37+
});
38+
39+
it('should return correct duration for 1 hour, 2 minutes, 3 seconds', () => {
40+
const duration = getFormattedEventsDuration(
41+
'2021-01-01T01:02:03Z',
42+
'2021-01-01T02:04:06Z'
43+
);
44+
expect(duration).toEqual(`mocked: ${60 * 60 + 2 * 60 + 3}s 0ms`);
45+
});
46+
47+
it('should handle endTime as null (use current time)', () => {
48+
const start = new Date(mockNow.getTime() - 60000).toISOString(); // 1 minute ago
49+
const duration = getFormattedEventsDuration(start, null);
50+
expect(duration).toEqual('mocked: 60s 0ms');
51+
});
52+
53+
it('should handle negative durations (start after end)', () => {
54+
const duration = getFormattedEventsDuration(
55+
'2021-01-01T01:00:00Z',
56+
'2021-01-01T00:00:00Z'
57+
);
58+
expect(duration).toEqual('mocked: -3600s 0ms');
59+
});
60+
61+
it('should handle numeric durations', () => {
62+
const duration = getFormattedEventsDuration(1726652232190, 1726652292194);
63+
expect(duration).toEqual('mocked: 60s 4ms');
64+
});
65+
66+
it('should remove ms from duration when hideMs is true', () => {
67+
const duration = getFormattedEventsDuration(
68+
1726652232190,
69+
1726652292194,
70+
true
71+
);
72+
expect(duration).toEqual('mocked: 60s');
73+
});
74+
75+
it('should not hide ms if there are no bigger units', () => {
76+
const duration = getFormattedEventsDuration(
77+
1726652232190,
78+
1726652232194,
79+
true
80+
);
81+
expect(duration).toEqual('mocked: 0s 4ms');
82+
});
83+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import formatDuration from '@/utils/data-formatters/format-duration';
2+
import dayjs from '@/utils/datetime/dayjs';
3+
4+
export default function getFormattedEventsDuration(
5+
startTime: Date | string | number | null,
6+
endTime: Date | string | number | null | undefined,
7+
hideMs: boolean = false
8+
) {
9+
const end = endTime ? dayjs(endTime) : dayjs();
10+
const start = dayjs(startTime);
11+
const diff = end.diff(start);
12+
const durationObj = dayjs.duration(diff);
13+
const seconds = Math.floor(durationObj.asSeconds());
14+
15+
const duration = formatDuration(
16+
{
17+
seconds: seconds.toString(),
18+
nanos: (durationObj.asMilliseconds() - seconds * 1000) * 1000000,
19+
},
20+
{ separator: ' ', minUnit: hideMs && seconds > 0 ? 's' : 'ms' }
21+
);
22+
23+
return duration;
24+
}

src/views/workflow-history-v2/hooks/use-event-group-duration.ts renamed to src/views/workflow-history-v2/workflow-history-event-group-duration/workflow-history-event-group-duration.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
import { useEffect, useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22

3-
import { type WorkflowExecutionCloseStatus } from '@/__generated__/proto-ts/uber/cadence/api/v1/WorkflowExecutionCloseStatus';
4-
import getFormattedEventsDuration from '@/views/workflow-history/workflow-history-events-duration-badge/helpers/get-formatted-events-duration';
3+
import getFormattedEventsDuration from './helpers/get-formatted-events-duration';
4+
import { type Props } from './workflow-history-event-group-duration.types';
55

6-
export default function useEventGroupDuration({
6+
export default function WorkflowHistoryEventGroupDuration({
77
startTime,
88
closeTime,
99
workflowIsArchived,
1010
workflowCloseStatus,
1111
eventsCount,
12-
loadingMoreEvents,
1312
hasMissingEvents,
13+
loadingMoreEvents,
1414
workflowCloseTime,
15-
}: {
16-
startTime: number | null | undefined;
17-
closeTime: number | null | undefined;
18-
workflowIsArchived: boolean;
19-
workflowCloseStatus: WorkflowExecutionCloseStatus | null | undefined;
20-
eventsCount: number;
21-
loadingMoreEvents: boolean;
22-
hasMissingEvents: boolean;
23-
workflowCloseTime: number | null | undefined;
24-
}) {
15+
}: Props) {
2516
const endTime = closeTime || workflowCloseTime;
2617
const workflowEnded =
2718
workflowIsArchived ||
@@ -55,5 +46,5 @@ export default function useEventGroupDuration({
5546
return null;
5647
}
5748

58-
return duration;
49+
return <>{duration}</>;
5950
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type WorkflowExecutionCloseStatus } from '@/__generated__/proto-ts/uber/cadence/api/v1/WorkflowExecutionCloseStatus';
2+
3+
export type Props = {
4+
startTime: number | null | undefined;
5+
closeTime: number | null | undefined;
6+
workflowIsArchived: boolean;
7+
workflowCloseStatus: WorkflowExecutionCloseStatus | null | undefined;
8+
eventsCount: number;
9+
loadingMoreEvents: boolean;
10+
hasMissingEvents: boolean;
11+
workflowCloseTime: number | null | undefined;
12+
};

src/views/workflow-history-v2/workflow-history-event-group/__tests__/workflow-history-event-group.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ jest.mock<typeof WorkflowHistoryTimelineResetButton>(
4545
))
4646
);
4747

48-
jest.mock('../../hooks/use-event-group-duration', () =>
49-
jest.fn(() => '1m 30s')
48+
jest.mock(
49+
'../../workflow-history-event-group-duration/workflow-history-event-group-duration',
50+
() => jest.fn(() => <span>1m 30s</span>)
5051
);
5152

5253
jest.mock('../helpers/get-event-group-filtering-type', () =>

src/views/workflow-history-v2/workflow-history-event-group/workflow-history-event-group.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import WorkflowHistoryGroupLabel from '@/views/workflow-history/workflow-history
99
import WorkflowHistoryTimelineResetButton from '@/views/workflow-history/workflow-history-timeline-reset-button/workflow-history-timeline-reset-button';
1010

1111
import workflowHistoryEventFilteringTypeColorsConfig from '../config/workflow-history-event-filtering-type-colors.config';
12-
import useEventGroupDuration from '../hooks/use-event-group-duration';
12+
import WorkflowHistoryEventGroupDuration from '../workflow-history-event-group-duration/workflow-history-event-group-duration';
1313

1414
import getEventGroupFilteringType from './helpers/get-event-group-filtering-type';
1515
import {
@@ -54,17 +54,6 @@ export default function WorkflowHistoryEventGroup({
5454
}
5555
}, [onReset]);
5656

57-
const eventGroupDuration = useEventGroupDuration({
58-
startTime: startTimeMs,
59-
closeTime: closeTimeMs,
60-
workflowIsArchived,
61-
eventsCount: events.length,
62-
workflowCloseStatus,
63-
loadingMoreEvents: showLoadingMoreEvents,
64-
hasMissingEvents,
65-
workflowCloseTime: workflowCloseTimeMs,
66-
});
67-
6857
return (
6958
<Panel
7059
title={
@@ -87,7 +76,16 @@ export default function WorkflowHistoryEventGroup({
8776
{eventsMetadata[eventsMetadata.length - 1].label}
8877
</styled.StatusContainer>
8978
<div>{eventGroup.timeMs ? formatDate(eventGroup.timeMs) : null}</div>
90-
<div>{eventGroupDuration}</div>
79+
<WorkflowHistoryEventGroupDuration
80+
startTime={startTimeMs}
81+
closeTime={closeTimeMs}
82+
workflowIsArchived={workflowIsArchived}
83+
eventsCount={events.length}
84+
workflowCloseStatus={workflowCloseStatus}
85+
loadingMoreEvents={showLoadingMoreEvents}
86+
hasMissingEvents={hasMissingEvents}
87+
workflowCloseTime={workflowCloseTimeMs}
88+
/>
9189
{/* TODO: add as event details:
9290
- Existing event details
9391
- Badges

0 commit comments

Comments
 (0)