@shipfox/client-workflows 15.0.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/preview.css +4 -0
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +21 -0
- package/dist/components/job-detail/job-detail-header.d.ts +8 -1
- package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-header.js +25 -12
- package/dist/components/job-detail/job-detail-header.js.map +1 -1
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-view.js +17 -2
- package/dist/components/job-detail/job-detail-view.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +1 -0
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +3 -0
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/index.js +3 -0
- package/dist/components/workflow-run-tabs/index.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +32 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.js +202 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +87 -22
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
- package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
- package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
- package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.js +9 -3
- package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +1 -3
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +150 -52
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/core/run-annotation.d.ts +73 -0
- package/dist/core/run-annotation.d.ts.map +1 -0
- package/dist/core/run-annotation.js +125 -0
- package/dist/core/run-annotation.js.map +1 -0
- package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
- package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
- package/dist/hooks/api/run-annotation-mapper.js +22 -0
- package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
- package/dist/hooks/api/run-annotations.d.ts +25 -0
- package/dist/hooks/api/run-annotations.d.ts.map +1 -0
- package/dist/hooks/api/run-annotations.js +78 -0
- package/dist/hooks/api/run-annotations.js.map +1 -0
- package/dist/routes/inputs.d.ts +7 -2
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +5 -6
- package/dist/routes/inputs.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/src/components/job-detail/job-detail-header.test.tsx +3 -0
- package/src/components/job-detail/job-detail-header.tsx +30 -5
- package/src/components/job-detail/job-detail-view.stories.tsx +5 -0
- package/src/components/job-detail/job-detail-view.tsx +18 -1
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +1 -1
- package/src/components/workflow-run-tabs/index.ts +3 -0
- package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
- package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
- package/src/components/workflow-run-tabs/run-annotation-list.tsx +258 -0
- package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
- package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +92 -17
- package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
- package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
- package/src/components/workflow-run-view/run-workspace-nav.test.tsx +44 -0
- package/src/components/workflow-run-view/run-workspace-nav.tsx +15 -2
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
- package/src/components/workflow-run-view/workflow-run-view.tsx +148 -30
- package/src/core/run-annotation.test.ts +315 -0
- package/src/core/run-annotation.ts +238 -0
- package/src/hooks/api/run-annotation-mapper.ts +30 -0
- package/src/hooks/api/run-annotations.ts +137 -0
- package/src/pages/workflow-job-detail-page.test.tsx +66 -0
- package/src/routes/inputs.ts +7 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/core/workflow-run-tabs.d.ts +0 -9
- package/dist/core/workflow-run-tabs.d.ts.map +0 -1
- package/dist/core/workflow-run-tabs.js +0 -3
- package/dist/core/workflow-run-tabs.js.map +0 -1
- package/src/core/workflow-run-tabs.ts +0 -8
|
@@ -6,13 +6,13 @@ import {cn} from '@shipfox/react-ui/utils';
|
|
|
6
6
|
import {Link} from '@tanstack/react-router';
|
|
7
7
|
import {useEffect, useMemo, useRef, useState} from 'react';
|
|
8
8
|
import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
|
|
9
|
+
import type {RunAnnotationSummary} from '#core/run-annotation.js';
|
|
9
10
|
import {
|
|
10
11
|
defaultJobExecution,
|
|
11
12
|
deriveJobDisplayStatus,
|
|
12
13
|
type Job,
|
|
13
14
|
type WorkflowRunDetail,
|
|
14
15
|
} from '#core/workflow-run.js';
|
|
15
|
-
import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
|
|
16
16
|
import {
|
|
17
17
|
type WorkflowJobSearch,
|
|
18
18
|
type WorkflowRunTab,
|
|
@@ -224,6 +224,7 @@ function RunWorkspaceNavContent({
|
|
|
224
224
|
section="annotations"
|
|
225
225
|
current={!currentJobId && activeSection === 'annotations'}
|
|
226
226
|
count={annotationSummary?.total}
|
|
227
|
+
countTruncated={annotationSummary?.truncated}
|
|
227
228
|
onNavigate={onNavigate}
|
|
228
229
|
/>
|
|
229
230
|
</li>
|
|
@@ -252,6 +253,7 @@ function RunSectionLink({
|
|
|
252
253
|
section,
|
|
253
254
|
current,
|
|
254
255
|
count,
|
|
256
|
+
countTruncated = false,
|
|
255
257
|
onNavigate,
|
|
256
258
|
}: {
|
|
257
259
|
workspaceSlug: string;
|
|
@@ -261,6 +263,8 @@ function RunSectionLink({
|
|
|
261
263
|
section: RunWorkspaceSection;
|
|
262
264
|
current: boolean;
|
|
263
265
|
count?: number | undefined;
|
|
266
|
+
/** The read hit its page budget, so the count is a lower bound and renders as `N+`. */
|
|
267
|
+
countTruncated?: boolean | undefined;
|
|
264
268
|
onNavigate?: (() => void) | undefined;
|
|
265
269
|
}) {
|
|
266
270
|
return (
|
|
@@ -275,6 +279,11 @@ function RunSectionLink({
|
|
|
275
279
|
}
|
|
276
280
|
onClick={onNavigate}
|
|
277
281
|
aria-current={current ? 'page' : undefined}
|
|
282
|
+
aria-label={
|
|
283
|
+
count
|
|
284
|
+
? `${sectionLabel(section)}, ${countTruncated ? `${count} or more` : count}`
|
|
285
|
+
: undefined
|
|
286
|
+
}
|
|
278
287
|
className={cn(
|
|
279
288
|
'flex min-h-32 items-center gap-8 rounded-4 px-8 text-xs font-medium text-foreground-neutral-base outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
|
|
280
289
|
current && 'bg-background-neutral-hover',
|
|
@@ -282,8 +291,12 @@ function RunSectionLink({
|
|
|
282
291
|
>
|
|
283
292
|
{sectionLabel(section)}
|
|
284
293
|
{count ? (
|
|
285
|
-
<span
|
|
294
|
+
<span
|
|
295
|
+
aria-hidden="true"
|
|
296
|
+
className="ml-auto font-code text-xs text-foreground-neutral-muted tabular-nums"
|
|
297
|
+
>
|
|
286
298
|
{count}
|
|
299
|
+
{countTruncated ? '+' : ''}
|
|
287
300
|
</span>
|
|
288
301
|
) : null}
|
|
289
302
|
</Link>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import type {AnnotationDto} from '@shipfox/annotations-dto';
|
|
1
2
|
import type {WorkflowRunDetailResponseDto} from '@shipfox/api-workflows-dto';
|
|
2
3
|
import {configureApiClient} from '@shipfox/client-api';
|
|
3
4
|
import {screen, waitFor, within} from '@testing-library/react';
|
|
4
5
|
import userEvent from '@testing-library/user-event';
|
|
5
6
|
import {
|
|
6
7
|
workflowJobDto,
|
|
8
|
+
workflowJobExecutionDto,
|
|
7
9
|
workflowRunDetailDto,
|
|
10
|
+
workflowStepAttemptDto,
|
|
8
11
|
workflowStepDto,
|
|
9
12
|
} from '#test/fixtures/workflow-run.js';
|
|
10
13
|
import {jsonResponse, PROJECT_TEST_WSLUG, renderProjectPage} from '#test/pages.js';
|
|
@@ -14,6 +17,13 @@ const RUN_ID = '66666666-6666-4666-8666-666666666666';
|
|
|
14
17
|
const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
|
|
15
18
|
const BUILD_JOB_ID = '77777777-7777-4777-8777-777777777777';
|
|
16
19
|
const DEPLOY_JOB_ID = '88888888-8888-4888-8888-888888888888';
|
|
20
|
+
const BUILD_EXECUTION_ID = '99999999-9999-4999-8999-000000000001';
|
|
21
|
+
const BUILD_STEP_ID = '55555555-5555-4555-8555-000000000001';
|
|
22
|
+
const BUILD_ATTEMPT_ID = '66666666-6666-4666-8666-000000000001';
|
|
23
|
+
const ANNOTATION_ID_ONE = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000001';
|
|
24
|
+
const ANNOTATION_ID_TWO = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000002';
|
|
25
|
+
const TASK_NINE_PATTERN = /Task nine/;
|
|
26
|
+
const SHOW_MORE_PATTERN = /Show \d+ more/;
|
|
17
27
|
|
|
18
28
|
describe('WorkflowRunView', () => {
|
|
19
29
|
beforeEach(() => {
|
|
@@ -32,9 +42,7 @@ describe('WorkflowRunView', () => {
|
|
|
32
42
|
});
|
|
33
43
|
|
|
34
44
|
test('opens the all-jobs Summary on the dependency graph by default', async () => {
|
|
35
|
-
|
|
36
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
37
|
-
});
|
|
45
|
+
configureRunFetch();
|
|
38
46
|
|
|
39
47
|
const {container} = renderView();
|
|
40
48
|
|
|
@@ -59,9 +67,7 @@ describe('WorkflowRunView', () => {
|
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
test('treats the removed Jobs tab URL as the graph Summary', async () => {
|
|
62
|
-
|
|
63
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
64
|
-
});
|
|
70
|
+
configureRunFetch();
|
|
65
71
|
|
|
66
72
|
renderView({tab: 'jobs'});
|
|
67
73
|
|
|
@@ -71,9 +77,7 @@ describe('WorkflowRunView', () => {
|
|
|
71
77
|
|
|
72
78
|
test('navigates graph and rail jobs to their dedicated job routes', async () => {
|
|
73
79
|
const user = userEvent.setup();
|
|
74
|
-
|
|
75
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
76
|
-
});
|
|
80
|
+
configureRunFetch();
|
|
77
81
|
|
|
78
82
|
const {router} = renderView();
|
|
79
83
|
await user.click(await screen.findByRole('button', {name: 'deploy, Running'}));
|
|
@@ -87,9 +91,7 @@ describe('WorkflowRunView', () => {
|
|
|
87
91
|
|
|
88
92
|
test('keeps run Annotations and Source in the workspace navigation', async () => {
|
|
89
93
|
const user = userEvent.setup();
|
|
90
|
-
|
|
91
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
92
|
-
});
|
|
94
|
+
configureRunFetch();
|
|
93
95
|
|
|
94
96
|
const {router} = renderView();
|
|
95
97
|
await screen.findByRole('region', {name: 'Workflow jobs'});
|
|
@@ -99,19 +101,182 @@ describe('WorkflowRunView', () => {
|
|
|
99
101
|
});
|
|
100
102
|
|
|
101
103
|
test('filters the single run-level Annotations page by job', async () => {
|
|
102
|
-
|
|
103
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
104
|
-
});
|
|
104
|
+
configureRunFetch();
|
|
105
105
|
|
|
106
106
|
renderView({tab: 'annotations', selection: {jobId: BUILD_JOB_ID}});
|
|
107
107
|
|
|
108
108
|
expect(
|
|
109
109
|
await screen.findByRole('combobox', {name: 'Filter annotations by job'}),
|
|
110
110
|
).toHaveTextContent('build');
|
|
111
|
-
expect(screen.
|
|
111
|
+
expect(await screen.findByText('This run has no annotations to show.')).toBeInTheDocument();
|
|
112
112
|
expect(screen.queryByRole('tab', {name: 'Annotations'})).not.toBeInTheDocument();
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
test('renders annotation bodies once, severity first, with provenance', async () => {
|
|
116
|
+
configureRunFetch([
|
|
117
|
+
annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info', sequence: 1}),
|
|
118
|
+
annotationDto({
|
|
119
|
+
id: ANNOTATION_ID_TWO,
|
|
120
|
+
context: 'smoke check',
|
|
121
|
+
style: 'error',
|
|
122
|
+
sequence: 2,
|
|
123
|
+
body: 'Task nine **failed**.',
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
renderView({tab: 'annotations'});
|
|
128
|
+
|
|
129
|
+
const headings = await screen.findAllByRole('heading', {level: 3});
|
|
130
|
+
expect(headings.map((heading) => heading.textContent)).toEqual(['smoke check', 'coverage']);
|
|
131
|
+
expect(screen.getByText(TASK_NINE_PATTERN)).toBeInTheDocument();
|
|
132
|
+
expect(screen.getAllByText('build · checkout · attempt 1')).toHaveLength(2);
|
|
133
|
+
expect(screen.getByText('2 annotations')).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('links an annotation back to the step that emitted it', async () => {
|
|
137
|
+
configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage'})]);
|
|
138
|
+
|
|
139
|
+
renderView({tab: 'annotations'});
|
|
140
|
+
|
|
141
|
+
const href = (await screen.findByRole('link', {name: 'Open step'})).getAttribute('href') ?? '';
|
|
142
|
+
const [pathname, query = ''] = href.split('?');
|
|
143
|
+
|
|
144
|
+
expect(pathname).toBe(`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}/jobs/${BUILD_JOB_ID}`);
|
|
145
|
+
expect(Object.fromEntries(new URLSearchParams(query))).toEqual({
|
|
146
|
+
jobExecution: BUILD_EXECUTION_ID,
|
|
147
|
+
step: BUILD_STEP_ID,
|
|
148
|
+
stepAttempt: BUILD_ATTEMPT_ID,
|
|
149
|
+
runAttempt: '"1"',
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('shows a failed annotations read as an error, never as an empty run', async () => {
|
|
154
|
+
configureApiClient({
|
|
155
|
+
fetchImpl: vi.fn((input: RequestInfo | URL) =>
|
|
156
|
+
Promise.resolve(
|
|
157
|
+
requestUrl(input).includes('/annotations')
|
|
158
|
+
? jsonResponse({code: 'internal'}, {status: 500})
|
|
159
|
+
: jsonResponse(workflowRunViewDetailDto()),
|
|
160
|
+
),
|
|
161
|
+
),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
renderView({tab: 'annotations'});
|
|
165
|
+
|
|
166
|
+
expect(await screen.findByRole('button', {name: 'Retry loading annotations'})).toBeVisible();
|
|
167
|
+
expect(screen.queryByText('This run has no annotations to show.')).not.toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('bounds how many annotation bodies render at once', async () => {
|
|
171
|
+
configureRunFetch(
|
|
172
|
+
Array.from({length: 30}, (_unused, index) =>
|
|
173
|
+
annotationDto({
|
|
174
|
+
id: `aaaaaaaa-aaaa-4aaa-8aaa-${String(index).padStart(12, '0')}`,
|
|
175
|
+
context: `context-${index}`,
|
|
176
|
+
sequence: index + 1,
|
|
177
|
+
}),
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
renderView({tab: 'annotations'});
|
|
182
|
+
|
|
183
|
+
expect(await screen.findAllByRole('heading', {level: 3})).toHaveLength(25);
|
|
184
|
+
const showMore = screen.getByRole('button', {name: 'Show 5 more of 30'});
|
|
185
|
+
|
|
186
|
+
await userEvent.click(showMore);
|
|
187
|
+
|
|
188
|
+
expect(screen.getAllByRole('heading', {level: 3})).toHaveLength(30);
|
|
189
|
+
expect(screen.queryByRole('button', {name: SHOW_MORE_PATTERN})).not.toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('focuses a deep-linked annotation beyond the render window', async () => {
|
|
193
|
+
const deepLinkedId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000029';
|
|
194
|
+
configureRunFetch(
|
|
195
|
+
Array.from({length: 30}, (_unused, index) =>
|
|
196
|
+
annotationDto({
|
|
197
|
+
id:
|
|
198
|
+
index === 29
|
|
199
|
+
? deepLinkedId
|
|
200
|
+
: `aaaaaaaa-aaaa-4aaa-8aaa-${String(index).padStart(12, '0')}`,
|
|
201
|
+
context: `context-${index}`,
|
|
202
|
+
sequence: index + 1,
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
renderView({tab: 'annotations', selection: {annotation: deepLinkedId}});
|
|
208
|
+
|
|
209
|
+
const target = await screen.findByRole('heading', {level: 3, name: 'context-29'});
|
|
210
|
+
const selected = target.closest('li');
|
|
211
|
+
await waitFor(() => expect(selected).toHaveFocus());
|
|
212
|
+
expect(selected).toHaveAttribute('aria-current', 'true');
|
|
213
|
+
expect(selected).toHaveClass('shadow-border-interactive-with-active');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('separates a filtered miss from a run with no annotations', async () => {
|
|
217
|
+
configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})]);
|
|
218
|
+
|
|
219
|
+
renderView({tab: 'annotations', selection: {severity: 'error'}});
|
|
220
|
+
|
|
221
|
+
expect(await screen.findByText('No matching annotations')).toBeInTheDocument();
|
|
222
|
+
expect(
|
|
223
|
+
screen.getByText('This run has annotations, but none at error severity.'),
|
|
224
|
+
).toBeInTheDocument();
|
|
225
|
+
expect(screen.getByRole('button', {name: 'Clear filters'})).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('does not claim a filtered miss beyond the loaded annotation budget', async () => {
|
|
229
|
+
configureRunFetch(
|
|
230
|
+
[annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})],
|
|
231
|
+
{},
|
|
232
|
+
{has_more: true, next_cursor: 'next-page'},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
renderView({tab: 'annotations', selection: {severity: 'error'}});
|
|
236
|
+
|
|
237
|
+
expect(await screen.findByText('No matches in loaded annotations')).toBeInTheDocument();
|
|
238
|
+
expect(
|
|
239
|
+
screen.getByText(
|
|
240
|
+
'None of the loaded annotations are at error severity. Load more annotations to continue searching.',
|
|
241
|
+
),
|
|
242
|
+
).toBeInTheDocument();
|
|
243
|
+
expect(screen.getByRole('button', {name: 'Load more annotations'})).toBeInTheDocument();
|
|
244
|
+
expect(screen.queryByText('No matching annotations')).not.toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('clears annotation filters and legacy step selection together', async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})]);
|
|
250
|
+
|
|
251
|
+
const {router} = renderView({
|
|
252
|
+
tab: 'annotations',
|
|
253
|
+
selection: {
|
|
254
|
+
jobId: DEPLOY_JOB_ID,
|
|
255
|
+
jobExecutionId: BUILD_EXECUTION_ID,
|
|
256
|
+
stepId: BUILD_STEP_ID,
|
|
257
|
+
stepAttemptId: BUILD_ATTEMPT_ID,
|
|
258
|
+
severity: 'error',
|
|
259
|
+
annotation: ANNOTATION_ID_ONE,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await user.click(await screen.findByRole('button', {name: 'Clear filters'}));
|
|
264
|
+
|
|
265
|
+
await waitFor(() => expect(router.state.location.search).toEqual({tab: 'annotations'}));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('blames only the filters that are actually active', async () => {
|
|
269
|
+
// The annotation belongs to build, so filtering to deploy is a job-only miss. Naming
|
|
270
|
+
// severity here would send the reader to change a control they never touched.
|
|
271
|
+
configureRunFetch([annotationDto({id: ANNOTATION_ID_ONE, context: 'coverage', style: 'info'})]);
|
|
272
|
+
|
|
273
|
+
renderView({tab: 'annotations', selection: {jobId: DEPLOY_JOB_ID}});
|
|
274
|
+
|
|
275
|
+
expect(
|
|
276
|
+
await screen.findByText('This run has annotations, but none from deploy.'),
|
|
277
|
+
).toBeInTheDocument();
|
|
278
|
+
});
|
|
279
|
+
|
|
115
280
|
test('renders the captured workflow source in the Source section', async () => {
|
|
116
281
|
configureApiClient({
|
|
117
282
|
fetchImpl: vi.fn(() =>
|
|
@@ -132,9 +297,7 @@ describe('WorkflowRunView', () => {
|
|
|
132
297
|
});
|
|
133
298
|
|
|
134
299
|
test('explains when the run has no source snapshot', async () => {
|
|
135
|
-
|
|
136
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
137
|
-
});
|
|
300
|
+
configureRunFetch();
|
|
138
301
|
|
|
139
302
|
renderView({tab: 'source'});
|
|
140
303
|
|
|
@@ -164,6 +327,48 @@ function renderView(props: Partial<Parameters<typeof WorkflowRunView>[0]> = {})
|
|
|
164
327
|
));
|
|
165
328
|
}
|
|
166
329
|
|
|
330
|
+
/** The API client hands `fetchImpl` a `Request`, whose URL only `.url` exposes. */
|
|
331
|
+
function requestUrl(input: RequestInfo | URL): string {
|
|
332
|
+
if (typeof input === 'string') return input;
|
|
333
|
+
return input instanceof URL ? input.href : input.url;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Routes the run detail and the annotations read, which are two independent fetches. */
|
|
337
|
+
function configureRunFetch(
|
|
338
|
+
annotations: AnnotationDto[] = [],
|
|
339
|
+
runOverrides: Partial<WorkflowRunDetailResponseDto> = {},
|
|
340
|
+
annotationPageOverrides: Partial<{has_more: boolean; next_cursor: string | null}> = {},
|
|
341
|
+
) {
|
|
342
|
+
configureApiClient({
|
|
343
|
+
fetchImpl: vi.fn((input: RequestInfo | URL) =>
|
|
344
|
+
Promise.resolve(
|
|
345
|
+
requestUrl(input).includes('/annotations')
|
|
346
|
+
? jsonResponse({
|
|
347
|
+
annotations,
|
|
348
|
+
has_more: false,
|
|
349
|
+
next_cursor: null,
|
|
350
|
+
...annotationPageOverrides,
|
|
351
|
+
})
|
|
352
|
+
: jsonResponse(workflowRunViewDetailDto(runOverrides)),
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function annotationDto(overrides: Partial<AnnotationDto> & {id: string}): AnnotationDto {
|
|
359
|
+
return {
|
|
360
|
+
job_id: BUILD_JOB_ID,
|
|
361
|
+
job_execution_id: BUILD_EXECUTION_ID,
|
|
362
|
+
origin_step_id: BUILD_STEP_ID,
|
|
363
|
+
origin_step_attempt: 1,
|
|
364
|
+
context: 'default',
|
|
365
|
+
style: 'default',
|
|
366
|
+
sequence: 1,
|
|
367
|
+
body: 'Body',
|
|
368
|
+
...overrides,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
167
372
|
function workflowRunViewDetailDto(
|
|
168
373
|
overrides: Partial<WorkflowRunDetailResponseDto> = {},
|
|
169
374
|
): WorkflowRunDetailResponseDto {
|
|
@@ -181,7 +386,28 @@ function workflowRunViewDetailDto(
|
|
|
181
386
|
run_attempt_id: RUN_ID,
|
|
182
387
|
name: 'build',
|
|
183
388
|
status: 'succeeded',
|
|
184
|
-
|
|
389
|
+
job_executions: [
|
|
390
|
+
workflowJobExecutionDto({
|
|
391
|
+
id: BUILD_EXECUTION_ID,
|
|
392
|
+
job_id: BUILD_JOB_ID,
|
|
393
|
+
status: 'succeeded',
|
|
394
|
+
steps: [
|
|
395
|
+
workflowStepDto({
|
|
396
|
+
id: BUILD_STEP_ID,
|
|
397
|
+
name: 'checkout',
|
|
398
|
+
status: 'succeeded',
|
|
399
|
+
attempts: [
|
|
400
|
+
workflowStepAttemptDto({
|
|
401
|
+
id: BUILD_ATTEMPT_ID,
|
|
402
|
+
step_id: BUILD_STEP_ID,
|
|
403
|
+
attempt: 1,
|
|
404
|
+
status: 'succeeded',
|
|
405
|
+
}),
|
|
406
|
+
],
|
|
407
|
+
}),
|
|
408
|
+
],
|
|
409
|
+
}),
|
|
410
|
+
],
|
|
185
411
|
}),
|
|
186
412
|
workflowJobDto({
|
|
187
413
|
id: DEPLOY_JOB_ID,
|