Skip to content

Commit a15cc7d

Browse files
Load and display Workflow Diagnostics in a placeholder component (#948)
Load and display workflow diagnostics in a placeholder component Add toggle between list and JSON view for diagnostics Fix type for diagnostics response fixture
1 parent 62f307c commit a15cc7d

File tree

9 files changed

+473
-8
lines changed

9 files changed

+473
-8
lines changed

src/route-handlers/diagnose-workflow/__fixtures__/mock-workflow-diagnostics-result.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { type WorkflowDiagnosticsResult } from '../diagnose-workflow.types';
2+
13
export const mockWorkflowDiagnosticsResult = {
24
DiagnosticsResult: {
35
Timeouts: null,
@@ -103,4 +105,4 @@ export const mockWorkflowDiagnosticsResult = {
103105
Retries: null,
104106
},
105107
DiagnosticsCompleted: true,
106-
};
108+
} as const satisfies WorkflowDiagnosticsResult;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { Suspense } from 'react';
2+
3+
import { HttpResponse } from 'msw';
4+
5+
import { render, screen } from '@/test-utils/rtl';
6+
7+
import ErrorBoundary from '@/components/error-boundary/error-boundary';
8+
9+
import WorkflowDiagnostics from '../workflow-diagnostics';
10+
11+
jest.mock('@/components/error-panel/error-panel', () =>
12+
jest.fn(({ message }) => <div data-testid="error-panel">{message}</div>)
13+
);
14+
15+
jest.mock('@/components/panel-section/panel-section', () =>
16+
jest.fn(({ children }) => <div data-testid="panel-section">{children}</div>)
17+
);
18+
19+
jest.mock('../workflow-diagnostics-content/workflow-diagnostics-content', () =>
20+
jest.fn(({ domain, cluster, workflowId, runId }) => (
21+
<div data-testid="workflow-diagnostics-content">
22+
<div>Domain: {domain}</div>
23+
<div>Cluster: {cluster}</div>
24+
<div>Workflow ID: {workflowId}</div>
25+
<div>Run ID: {runId}</div>
26+
</div>
27+
))
28+
);
29+
30+
jest.mock('../config/workflow-diagnostics-disabled-error-panel.config', () => ({
31+
message: 'Workflow Diagnostics is currently disabled',
32+
}));
33+
34+
describe(WorkflowDiagnostics.name, () => {
35+
beforeEach(() => {
36+
jest.clearAllMocks();
37+
});
38+
39+
it('should render workflow diagnostics content when diagnostics is enabled', async () => {
40+
await setup({ isDiagnosticsEnabled: true });
41+
42+
await screen.findByTestId('workflow-diagnostics-content');
43+
44+
expect(
45+
screen.getByTestId('workflow-diagnostics-content')
46+
).toBeInTheDocument();
47+
expect(screen.getByText('Domain: test-domain')).toBeInTheDocument();
48+
expect(screen.getByText('Cluster: test-cluster')).toBeInTheDocument();
49+
expect(
50+
screen.getByText('Workflow ID: test-workflow-id')
51+
).toBeInTheDocument();
52+
expect(screen.getByText('Run ID: test-run-id')).toBeInTheDocument();
53+
});
54+
55+
it('should render error panel when diagnostics is disabled', async () => {
56+
await setup({ isDiagnosticsEnabled: false });
57+
58+
await screen.findByTestId('error-panel');
59+
60+
expect(screen.getByTestId('panel-section')).toBeInTheDocument();
61+
expect(
62+
screen.getByText('Workflow Diagnostics is currently disabled')
63+
).toBeInTheDocument();
64+
});
65+
66+
it('should handle config API errors gracefully', async () => {
67+
await setup({ isDiagnosticsEnabled: false, error: true });
68+
69+
expect(
70+
await screen.findByText('Error: Failed to fetch config')
71+
).toBeInTheDocument();
72+
});
73+
});
74+
75+
async function setup({
76+
isDiagnosticsEnabled,
77+
error,
78+
}: {
79+
isDiagnosticsEnabled: boolean;
80+
error?: boolean;
81+
}) {
82+
render(
83+
<ErrorBoundary
84+
fallbackRender={({ error }) => <div>Error: {error.message}</div>}
85+
>
86+
<Suspense fallback={<div>Loading...</div>}>
87+
<WorkflowDiagnostics
88+
params={{
89+
cluster: 'test-cluster',
90+
domain: 'test-domain',
91+
runId: 'test-run-id',
92+
workflowId: 'test-workflow-id',
93+
workflowTab: 'diagnostics',
94+
}}
95+
/>
96+
</Suspense>
97+
</ErrorBoundary>,
98+
{
99+
endpointsMocks: [
100+
{
101+
path: '/api/config',
102+
httpMethod: 'GET',
103+
mockOnce: false,
104+
httpResolver: async () => {
105+
if (error) {
106+
return HttpResponse.json(
107+
{ message: 'Failed to fetch config' },
108+
{ status: 500 }
109+
);
110+
} else {
111+
return HttpResponse.json(isDiagnosticsEnabled ?? false);
112+
}
113+
},
114+
},
115+
],
116+
}
117+
);
118+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { HttpResponse } from 'msw';
2+
import { ZodError } from 'zod';
3+
4+
import { render, screen, userEvent, within } from '@/test-utils/rtl';
5+
6+
import ErrorBoundary from '@/components/error-boundary/error-boundary';
7+
import { mockWorkflowDiagnosticsResult } from '@/route-handlers/diagnose-workflow/__fixtures__/mock-workflow-diagnostics-result';
8+
import { type DiagnoseWorkflowResponse } from '@/route-handlers/diagnose-workflow/diagnose-workflow.types';
9+
10+
import WorkflowDiagnosticsContent from '../workflow-diagnostics-content';
11+
12+
jest.mock(
13+
'@/components/section-loading-indicator/section-loading-indicator',
14+
() => jest.fn(() => <div data-testid="loading-indicator">Loading...</div>)
15+
);
16+
17+
jest.mock('../../workflow-diagnostics-list/workflow-diagnostics-list', () =>
18+
jest.fn(() => <div>Diagnostics List Component</div>)
19+
);
20+
21+
jest.mock('../../workflow-diagnostics-json/workflow-diagnostics-json', () =>
22+
jest.fn(() => <div>Diagnostics JSON Component</div>)
23+
);
24+
25+
describe('WorkflowDiagnosticsContent', () => {
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
it('should render loading indicator when status is pending', async () => {
31+
setup({ responseType: 'pending' });
32+
33+
expect(await screen.findByTestId('loading-indicator')).toBeInTheDocument();
34+
});
35+
36+
it('should throw error when status is error', async () => {
37+
setup({ responseType: 'error' });
38+
39+
expect(
40+
await screen.findByText('Failed to fetch diagnostics')
41+
).toBeInTheDocument();
42+
});
43+
44+
it('should render diagnostics list view by default', async () => {
45+
setup({ responseType: 'success' });
46+
47+
expect(
48+
await screen.findByText('Diagnostics List Component')
49+
).toBeInTheDocument();
50+
expect(
51+
screen.queryByText('Diagnostics JSON Component')
52+
).not.toBeInTheDocument();
53+
});
54+
55+
it('should allow switching between list and JSON views', async () => {
56+
const { user } = setup({ responseType: 'success' });
57+
58+
// Initially shows list view
59+
expect(
60+
await screen.findByText('Diagnostics List Component')
61+
).toBeInTheDocument();
62+
expect(
63+
screen.queryByText('Diagnostics JSON Component')
64+
).not.toBeInTheDocument();
65+
66+
// Switch to JSON view
67+
const listbox = screen.getByRole('listbox');
68+
const jsonButton = within(listbox).getByRole('option', { name: /json/i });
69+
await user.click(jsonButton);
70+
71+
expect(screen.getByText('Diagnostics JSON Component')).toBeInTheDocument();
72+
expect(
73+
screen.queryByText('Diagnostics List Component')
74+
).not.toBeInTheDocument();
75+
76+
// Switch back to list view
77+
const listButton = within(listbox).getByRole('option', { name: /list/i });
78+
await user.click(listButton);
79+
80+
expect(screen.getByText('Diagnostics List Component')).toBeInTheDocument();
81+
expect(
82+
screen.queryByText('Diagnostics JSON Component')
83+
).not.toBeInTheDocument();
84+
});
85+
86+
it('should switch to JSON view when parsing error exists', async () => {
87+
setup({ responseType: 'parsing-error' });
88+
89+
expect(
90+
await screen.findByText('Diagnostics JSON Component')
91+
).toBeInTheDocument();
92+
expect(
93+
screen.queryByText('Diagnostics List Component')
94+
).not.toBeInTheDocument();
95+
});
96+
97+
it('should disable list view when parsing error exists', async () => {
98+
const { debug } = setup({ responseType: 'parsing-error' });
99+
100+
const listbox = await screen.findByRole('listbox');
101+
const listButton = within(listbox).getByRole('option', { name: /list/i });
102+
debug();
103+
expect(listButton).toBeDisabled();
104+
});
105+
});
106+
107+
function setup({
108+
responseType,
109+
}: {
110+
responseType?: 'success' | 'error' | 'parsing-error' | 'pending';
111+
} = {}) {
112+
const user = userEvent.setup();
113+
114+
const renderResult = render(
115+
<ErrorBoundary fallbackRender={({ error }) => <div>{error.message}</div>}>
116+
<WorkflowDiagnosticsContent
117+
domain="test-domain"
118+
cluster="test-cluster"
119+
workflowId="test-workflow-id"
120+
runId="test-run-id"
121+
/>
122+
</ErrorBoundary>,
123+
{
124+
endpointsMocks: [
125+
{
126+
path: '/api/domains/:domain/:cluster/workflows/:workflowId/:runId/diagnose',
127+
httpMethod: 'GET',
128+
mockOnce: false,
129+
httpResolver: async () => {
130+
switch (responseType) {
131+
case 'error':
132+
return HttpResponse.json(
133+
{ message: 'Failed to fetch diagnostics' },
134+
{ status: 500 }
135+
);
136+
case 'parsing-error':
137+
return HttpResponse.json({
138+
result: { raw: 'invalid data' },
139+
parsingError: new ZodError([]),
140+
} satisfies DiagnoseWorkflowResponse);
141+
case 'pending':
142+
return new Promise(() => {}); // Never resolves to simulate pending
143+
case 'success':
144+
default:
145+
return HttpResponse.json({
146+
result: mockWorkflowDiagnosticsResult,
147+
parsingError: null,
148+
} satisfies DiagnoseWorkflowResponse);
149+
}
150+
},
151+
},
152+
],
153+
}
154+
);
155+
156+
return { user, ...renderResult };
157+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { styled as createStyled, withStyle } from 'baseui';
2+
import {
3+
type SegmentedControlOverrides,
4+
type SegmentOverrides,
5+
} from 'baseui/segmented-control';
6+
import { type Theme } from 'baseui/theme';
7+
import { type StyleObject } from 'styletron-react';
8+
9+
import PageSection from '@/components/page-section/page-section';
10+
11+
export const styled = {
12+
PageSection: withStyle(PageSection, ({ $theme }: { $theme: Theme }) => ({
13+
display: 'flex',
14+
flexDirection: 'column',
15+
gap: $theme.sizing.scale800,
16+
})),
17+
ButtonsContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
18+
display: 'flex',
19+
justifyContent: 'space-between',
20+
gap: $theme.sizing.scale300,
21+
})),
22+
};
23+
24+
export const overrides = {
25+
viewToggle: {
26+
Root: {
27+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
28+
height: $theme.sizing.scale950,
29+
padding: $theme.sizing.scale0,
30+
borderRadius: $theme.borders.radius300,
31+
...$theme.typography.ParagraphSmall,
32+
width: 'auto',
33+
flexGrow: 1,
34+
[$theme.mediaQuery.medium]: {
35+
flexGrow: 0,
36+
},
37+
}),
38+
},
39+
SegmentList: {
40+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
41+
height: $theme.sizing.scale950,
42+
...$theme.typography.ParagraphSmall,
43+
}),
44+
},
45+
Active: {
46+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
47+
height: $theme.sizing.scale900,
48+
top: 0,
49+
}),
50+
},
51+
} satisfies SegmentedControlOverrides,
52+
viewToggleSegment: {
53+
Segment: {
54+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
55+
height: $theme.sizing.scale900,
56+
whiteSpace: 'nowrap',
57+
}),
58+
},
59+
} satisfies SegmentOverrides,
60+
};

0 commit comments

Comments
 (0)