Skip to content

Commit e3b78b6

Browse files
Add unit tests and isolate prop types
Signed-off-by: Adhitya Mamallan <[email protected]>
1 parent 0d71532 commit e3b78b6

File tree

6 files changed

+197
-55
lines changed

6 files changed

+197
-55
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { useMemo } from 'react';
22

33
import WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group';
4-
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
54

65
import WorkflowHistoryPanelDetailsEntry from '../workflow-history-panel-details-entry/workflow-history-panel-details-entry';
76

87
import { styled } from './workflow-history-event-details.styles';
9-
import { type EventDetailsEntries } from './workflow-history-event-details.types';
8+
import {
9+
type Props,
10+
type EventDetailsEntries,
11+
} from './workflow-history-event-details.types';
1012

1113
export default function WorkflowHistoryEventDetails({
1214
eventDetails,
1315
workflowPageParams,
14-
}: {
15-
eventDetails: EventDetailsEntries;
16-
workflowPageParams: WorkflowPageParams;
17-
}) {
16+
}: Props) {
1817
const [panelDetails, restDetails] = useMemo(
1918
() =>
2019
eventDetails.reduce<[EventDetailsEntries, EventDetailsEntries]>(

src/views/workflow-history-v2/workflow-history-event-details/workflow-history-event-details.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
22

3+
export type Props = {
4+
eventDetails: EventDetailsEntries;
5+
workflowPageParams: WorkflowPageParams;
6+
};
7+
38
export type EventDetailsFuncArgs = {
49
path: string;
510
key: string;

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,17 @@ import { Button } from 'baseui/button';
44
import { ButtonGroup } from 'baseui/button-group';
55
import { MdClose } from 'react-icons/md';
66

7-
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
8-
97
import WorkflowHistoryEventDetails from '../workflow-history-event-details/workflow-history-event-details';
108

119
import { overrides, styled } from './workflow-history-group-details.styles';
12-
import { type GroupDetailsEntries } from './workflow-history-group-details.types';
10+
import { type Props } from './workflow-history-group-details.types';
1311

1412
export default function WorkflowHistoryGroupDetails({
1513
groupDetailsEntries,
1614
initialEventId,
1715
workflowPageParams,
1816
onClose,
19-
}: {
20-
groupDetailsEntries: GroupDetailsEntries;
21-
initialEventId: string | undefined;
22-
workflowPageParams: WorkflowPageParams;
23-
onClose?: () => void;
24-
}) {
17+
}: Props) {
2518
const [selectedIndex, setSelectedIndex] = useState<number>(
2619
(() => {
2720
const selectedIdx = groupDetailsEntries.findIndex(

src/views/workflow-history-v2/workflow-history-group-details/workflow-history-group-details.types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
2+
13
import { type EventDetailsEntries } from '../workflow-history-event-details/workflow-history-event-details.types';
24

35
export type EventDetailsTabContent = {
@@ -6,3 +8,10 @@ export type EventDetailsTabContent = {
68
};
79

810
export type GroupDetailsEntries = Array<[string, EventDetailsTabContent]>;
11+
12+
export type Props = {
13+
groupDetailsEntries: GroupDetailsEntries;
14+
initialEventId: string | undefined;
15+
workflowPageParams: WorkflowPageParams;
16+
onClose?: () => void;
17+
};
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { render, screen } from '@/test-utils/rtl';
2+
3+
import type WorkflowHistoryEventDetailsGroup from '@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group';
4+
import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types';
5+
6+
import {
7+
type EventDetailsGroupEntry,
8+
type EventDetailsSingleEntry,
9+
} from '../../workflow-history-event-details/workflow-history-event-details.types';
10+
import WorkflowHistoryPanelDetailsEntry from '../workflow-history-panel-details-entry';
11+
12+
jest.mock<typeof WorkflowHistoryEventDetailsGroup>(
13+
'@/views/workflow-history/workflow-history-event-details-group/workflow-history-event-details-group',
14+
() =>
15+
jest.fn(({ entries, parentGroupPath }) => (
16+
<div data-testid="event-details-group">
17+
<div>{`Event Details Group (${entries.length} entries)`}</div>
18+
<div>{parentGroupPath && ` - Parent: ${parentGroupPath}`}</div>
19+
</div>
20+
))
21+
);
22+
23+
describe(WorkflowHistoryPanelDetailsEntry.name, () => {
24+
it('renders a single entry with value', () => {
25+
const detail: EventDetailsSingleEntry = {
26+
key: 'test-key',
27+
path: 'test.path',
28+
value: 'test-value',
29+
isGroup: false,
30+
renderConfig: null,
31+
};
32+
33+
setup({ detail });
34+
35+
expect(screen.getByText('test.path')).toBeInTheDocument();
36+
expect(screen.getByText('test-value')).toBeInTheDocument();
37+
});
38+
39+
it('renders a group entry with WorkflowHistoryEventDetailsGroup', () => {
40+
const detail: EventDetailsGroupEntry = {
41+
key: 'test-key',
42+
path: 'test.group.path',
43+
isGroup: true,
44+
groupEntries: [
45+
{
46+
key: 'entry1',
47+
path: 'test.group.path.entry1',
48+
value: 'value1',
49+
isGroup: false,
50+
renderConfig: null,
51+
},
52+
{
53+
key: 'entry2',
54+
path: 'test.group.path.entry2',
55+
value: 'value2',
56+
isGroup: false,
57+
renderConfig: null,
58+
},
59+
],
60+
renderConfig: null,
61+
};
62+
63+
setup({ detail });
64+
65+
expect(screen.getByText('test.group.path')).toBeInTheDocument();
66+
expect(screen.getByTestId('event-details-group')).toBeInTheDocument();
67+
expect(
68+
screen.getByText('Event Details Group (2 entries)')
69+
).toBeInTheDocument();
70+
expect(screen.getByText(/Parent: test\.group\.path/)).toBeInTheDocument();
71+
});
72+
73+
it('renders a custom ValueComponent when provided for a single entry', () => {
74+
const MockValueComponent = jest.fn(
75+
({
76+
entryKey,
77+
entryPath,
78+
entryValue,
79+
isNegative,
80+
domain,
81+
cluster,
82+
}: {
83+
entryKey: string;
84+
entryPath: string;
85+
entryValue: any;
86+
isNegative?: boolean;
87+
domain: string;
88+
cluster: string;
89+
}) => (
90+
<div data-testid="custom-value-component">
91+
Custom: {entryKey} - {entryPath} - {JSON.stringify(entryValue)}
92+
{isNegative && ' (negative)'}
93+
{domain} - {cluster}
94+
</div>
95+
)
96+
);
97+
98+
const detail: EventDetailsSingleEntry = {
99+
key: 'test-key',
100+
path: 'test.path',
101+
value: 'test-value',
102+
isGroup: false,
103+
renderConfig: {
104+
name: 'Test Config',
105+
key: 'test-key',
106+
valueComponent: MockValueComponent,
107+
},
108+
};
109+
110+
setup({
111+
detail,
112+
workflowPageParams: {
113+
domain: 'test-domain',
114+
cluster: 'test-cluster',
115+
workflowId: 'test-workflow-id',
116+
runId: 'test-run-id',
117+
},
118+
});
119+
120+
expect(screen.getByTestId('custom-value-component')).toBeInTheDocument();
121+
expect(
122+
screen.getByText(/Custom: test-key - test\.path - "test-value"/)
123+
).toBeInTheDocument();
124+
expect(screen.getByText(/test-domain - test-cluster/)).toBeInTheDocument();
125+
});
126+
127+
it('does not render ValueComponent for group entries', () => {
128+
const MockValueComponent = jest.fn(() => (
129+
<div data-testid="custom-value-component">Custom Component</div>
130+
));
131+
132+
const detail: EventDetailsGroupEntry = {
133+
key: 'test-key',
134+
path: 'test.group.path',
135+
isGroup: true,
136+
groupEntries: [
137+
{
138+
key: 'entry1',
139+
path: 'test.group.path.entry1',
140+
value: 'value1',
141+
isGroup: false,
142+
renderConfig: null,
143+
},
144+
],
145+
renderConfig: {
146+
name: 'Test Config',
147+
key: 'test-key',
148+
valueComponent: MockValueComponent,
149+
},
150+
};
151+
152+
setup({ detail });
153+
154+
expect(
155+
screen.queryByTestId('custom-value-component')
156+
).not.toBeInTheDocument();
157+
expect(screen.getByTestId('event-details-group')).toBeInTheDocument();
158+
});
159+
});
160+
161+
function setup({
162+
detail,
163+
workflowPageParams = {
164+
domain: 'test-domain',
165+
cluster: 'test-cluster',
166+
workflowId: 'test-workflow-id',
167+
runId: 'test-run-id',
168+
},
169+
}: {
170+
detail: EventDetailsSingleEntry | EventDetailsGroupEntry;
171+
workflowPageParams?: WorkflowPageParams;
172+
}) {
173+
render(
174+
<WorkflowHistoryPanelDetailsEntry detail={detail} {...workflowPageParams} />
175+
);
176+
}

src/views/workflow-history/config/workflow-history-event-details.config.ts

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,17 @@ import { type WorkflowHistoryEventDetailsConfig } from '../workflow-history-even
99
import WorkflowHistoryEventDetailsJson from '../workflow-history-event-details-json/workflow-history-event-details-json';
1010
import WorkflowHistoryEventDetailsPlaceholderText from '../workflow-history-event-details-placeholder-text/workflow-history-event-details-placeholder-text';
1111

12-
/**
13-
* Configuration array for customizing how workflow history event details are rendered.
14-
* Each config entry defines matching criteria and rendering behavior for specific event fields.
15-
* Configs are evaluated in order, and the first matching config is applied to each field.
16-
*/
1712
const workflowHistoryEventDetailsConfig = [
18-
/**
19-
* Hides fields with null or undefined values from the event details display.
20-
*/
2113
{
2214
name: 'Filter empty value',
2315
customMatcher: ({ value }) => value === null || value === undefined,
2416
hide: () => true,
2517
},
26-
/**
27-
* Hides internal fields (taskId, eventType) that are not useful for display.
28-
*/
2918
{
3019
name: 'Filter unneeded values',
3120
pathRegex: '(taskId|eventType)$',
3221
hide: () => true,
3322
},
34-
/**
35-
* Displays a placeholder text for timeout/retry fields that are set to 0 (not configured).
36-
* Also removes the "Seconds" suffix from labels since formatted durations may be in minutes/hours.
37-
*/
3823
{
3924
name: 'Not set placeholder',
4025
customMatcher: ({ value, path }) => {
@@ -49,17 +34,11 @@ const workflowHistoryEventDetailsConfig = [
4934
valueComponent: () =>
5035
createElement(WorkflowHistoryEventDetailsPlaceholderText),
5136
},
52-
/**
53-
* Formats Date objects as human-readable time strings.
54-
*/
5537
{
5638
name: 'Date object as time string',
5739
customMatcher: ({ value }) => value instanceof Date,
5840
valueComponent: ({ entryValue }) => formatDate(entryValue),
5941
},
60-
/**
61-
* Renders task list names as clickable links that navigate to the task list view.
62-
*/
6342
{
6443
name: 'Tasklists as links',
6544
key: 'taskList',
@@ -71,32 +50,20 @@ const workflowHistoryEventDetailsConfig = [
7150
});
7251
},
7352
},
74-
/**
75-
* Renders JSON fields (input, result, details, etc.) as formatted PrettyJson components.
76-
* Uses forceWrap to ensure proper wrapping of long JSON content.
77-
*/
7853
{
7954
name: 'Json as PrettyJson',
8055
pathRegex:
8156
'(input|result|details|failureDetails|Error|lastCompletionResult|heartbeatDetails|lastFailureDetails)$',
8257
valueComponent: WorkflowHistoryEventDetailsJson,
8358
forceWrap: true,
8459
},
85-
/**
86-
* Formats duration fields (ending in TimeoutSeconds, BackoffSeconds, or InSeconds) as human-readable durations.
87-
* Removes the "Seconds" suffix from labels since formatted durations may be in minutes/hours.
88-
*/
8960
{
9061
name: 'Duration & interval seconds',
9162
pathRegex: '(TimeoutSeconds|BackoffSeconds|InSeconds)$',
9263
getLabel: ({ key }) => key.replace(/InSeconds|Seconds|$/, ''), // remove seconds suffix from label as formatted duration can be minutes/hours etc.
9364
valueComponent: ({ entryValue }) =>
9465
formatDuration({ seconds: entryValue > 0 ? entryValue : 0, nanos: 0 }),
9566
},
96-
/**
97-
* Renders workflow execution objects as clickable links that navigate to the workflow view.
98-
* Applies to parentWorkflowExecution, externalWorkflowExecution, and workflowExecution fields.
99-
*/
10067
{
10168
name: 'WorkflowExecution as link',
10269
pathRegex:
@@ -110,10 +77,6 @@ const workflowHistoryEventDetailsConfig = [
11077
});
11178
},
11279
},
113-
/**
114-
* Renders run ID fields as clickable links that navigate to the corresponding workflow run.
115-
* Applies to firstExecutionRunId, originalExecutionRunId, newExecutionRunId, and continuedExecutionRunId.
116-
*/
11780
{
11881
name: 'RunIds as link',
11982
pathRegex:
@@ -127,9 +90,6 @@ const workflowHistoryEventDetailsConfig = [
12790
});
12891
},
12992
},
130-
/**
131-
* Renames the "attempt" field label to "retryAttempt" for better clarity.
132-
*/
13393
{
13494
name: 'Retry config attempt as retryAttempt',
13595
key: 'attempt',

0 commit comments

Comments
 (0)