@shipfox/client-workflows 14.0.0 → 15.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.tsx +7 -2
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +35 -0
- package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -0
- package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.js +2 -2
- package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -0
- package/dist/components/job-detail/job-detail-header.d.ts +8 -0
- package/dist/components/job-detail/job-detail-header.d.ts.map +1 -0
- package/dist/components/job-detail/job-detail-header.js +111 -0
- package/dist/components/job-detail/job-detail-header.js.map +1 -0
- package/dist/components/job-detail/job-detail-view.d.ts +16 -0
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -0
- package/dist/components/job-detail/job-detail-view.js +405 -0
- package/dist/components/job-detail/job-detail-view.js.map +1 -0
- package/dist/components/job-detail/job-empty-states.d.ts +10 -0
- package/dist/components/job-detail/job-empty-states.d.ts.map +1 -0
- package/dist/components/job-detail/job-empty-states.js +195 -0
- package/dist/components/job-detail/job-empty-states.js.map +1 -0
- package/dist/components/job-detail/job-execution-switcher.d.ts.map +1 -0
- package/dist/components/job-detail/job-execution-switcher.js.map +1 -0
- package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -0
- package/dist/components/job-detail/job-execution-time-text.js.map +1 -0
- package/dist/components/job-detail/job-selection.d.ts +24 -0
- package/dist/components/job-detail/job-selection.d.ts.map +1 -0
- package/dist/components/job-detail/job-selection.js +66 -0
- package/dist/components/job-detail/job-selection.js.map +1 -0
- package/dist/components/job-detail/step-attempt-log-panel.d.ts +15 -0
- package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -0
- package/dist/components/{workflow-run-view → job-detail}/step-attempt-log-panel.js +61 -16
- package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -0
- package/dist/components/job-graph/job-graph-content.js +1 -1
- package/dist/components/job-graph/job-graph-content.js.map +1 -1
- package/dist/components/step-list/step-list-model.js +2 -2
- package/dist/components/step-list/step-list-model.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +1 -0
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js +29 -4
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-duration-label.d.ts +9 -4
- package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
- package/dist/components/workflow-run-duration-label.js +27 -7
- package/dist/components/workflow-run-duration-label.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +6 -5
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +68 -64
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +0 -3
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/index.js +0 -3
- package/dist/components/workflow-run-tabs/index.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +3 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.js +2 -2
- package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -1
- package/dist/components/workflow-run-view/index.d.ts +0 -1
- package/dist/components/workflow-run-view/index.d.ts.map +1 -1
- package/dist/components/workflow-run-view/index.js +0 -1
- package/dist/components/workflow-run-view/index.js.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts +20 -0
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -0
- package/dist/components/workflow-run-view/run-workspace-nav.js +271 -0
- package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-selection.js +1 -14
- package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -8
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +250 -236
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
- package/dist/components/workflow-status/status-visuals.js +10 -2
- package/dist/components/workflow-status/status-visuals.js.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.js +13 -2
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +9 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +13 -3
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/step-attempt.d.ts.map +1 -1
- package/dist/core/entities/step.d.ts +2 -0
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +13 -0
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +56 -1
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +98 -0
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/workflow-run.d.ts +4 -4
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js +3 -3
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/feature.d.ts +20 -16
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +5 -0
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/workflow-runs.d.ts +5 -3
- package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
- package/dist/hooks/api/workflow-runs.js +5 -4
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pages/workflow-job-detail-page.d.ts +11 -0
- package/dist/pages/workflow-job-detail-page.d.ts.map +1 -0
- package/dist/pages/workflow-job-detail-page.js +66 -0
- package/dist/pages/workflow-job-detail-page.js.map +1 -0
- package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-detail-page.js +4 -37
- package/dist/pages/workflow-run-detail-page.js.map +1 -1
- package/dist/routes/inputs.d.ts +47 -34
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +69 -37
- package/dist/routes/inputs.js.map +1 -1
- package/dist/routes/job-detail.d.ts +10 -0
- package/dist/routes/job-detail.d.ts.map +1 -0
- package/dist/routes/job-detail.js +27 -0
- package/dist/routes/job-detail.js.map +1 -0
- package/dist/routes/project-route.d.ts +1 -1
- package/dist/routes/run-detail.d.ts.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.stories.tsx +1 -1
- package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.tsx +2 -2
- package/src/components/job-detail/job-detail-header.test.tsx +31 -0
- package/src/components/job-detail/job-detail-header.tsx +111 -0
- package/src/components/job-detail/job-detail-view.stories.tsx +526 -0
- package/src/components/job-detail/job-detail-view.tsx +450 -0
- package/src/components/job-detail/job-empty-states.test.ts +27 -0
- package/src/components/job-detail/job-empty-states.tsx +248 -0
- package/src/components/job-detail/job-selection.test.ts +188 -0
- package/src/components/job-detail/job-selection.ts +96 -0
- package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.test.tsx +32 -9
- package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.tsx +52 -18
- package/src/components/job-graph/job-graph-content.tsx +1 -1
- package/src/components/step-list/step-list-model.test.ts +1 -1
- package/src/components/step-list/step-list-model.ts +2 -2
- package/src/components/step-list/step-list.stories.tsx +1 -1
- package/src/components/step-list/step-list.test.tsx +33 -0
- package/src/components/step-list/step-list.tsx +40 -9
- package/src/components/workflow-run-duration-label.tsx +50 -12
- package/src/components/workflow-run-list/workflow-run-row.tsx +6 -4
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +38 -0
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +79 -5
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +56 -60
- package/src/components/workflow-run-tabs/index.ts +0 -3
- package/src/components/workflow-run-tabs/run-annotations-empty.tsx +6 -2
- package/src/components/workflow-run-view/index.ts +0 -1
- package/src/components/workflow-run-view/run-workspace-nav.test.tsx +242 -0
- package/src/components/workflow-run-view/run-workspace-nav.tsx +301 -0
- package/src/components/workflow-run-view/workflow-run-selection.ts +1 -23
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +62 -1136
- package/src/components/workflow-run-view/workflow-run-view.tsx +330 -321
- package/src/components/workflow-status/status-visuals.ts +5 -2
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +1 -0
- package/src/components/workflow-status/workflow-status-icon.tsx +26 -1
- package/src/core/entities/job-execution.ts +17 -2
- package/src/core/entities/step.ts +26 -0
- package/src/core/entities/workflow-run-display.test.ts +159 -0
- package/src/core/entities/workflow-run.ts +139 -1
- package/src/core/workflow-run.ts +16 -1
- package/src/feature.ts +5 -0
- package/src/hooks/api/workflow-runs.ts +6 -2
- package/src/index.ts +1 -0
- package/src/page-harness-budget.test.ts +1 -0
- package/src/pages/workflow-job-detail-page.test.tsx +539 -0
- package/src/pages/workflow-job-detail-page.tsx +84 -0
- package/src/pages/workflow-run-detail-page.tsx +3 -43
- package/src/pages/workflow-run-pages.test.tsx +77 -332
- package/src/routes/inputs.test.ts +56 -6
- package/src/routes/inputs.ts +64 -17
- package/src/routes/job-detail.tsx +29 -0
- package/test/pages.tsx +25 -3
- package/test/render.tsx +6 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +0 -18
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-jobs-list.js +0 -107
- package/dist/components/workflow-run-tabs/run-jobs-list.js.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts +0 -6
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-count.js +0 -27
- package/dist/components/workflow-run-tabs/run-tab-count.js.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +0 -8
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-strip.js +0 -82
- package/dist/components/workflow-run-tabs/run-tab-strip.js.map +0 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +0 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +0 -1
- package/dist/components/workflow-run-view/job-card.d.ts +0 -17
- package/dist/components/workflow-run-view/job-card.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-card.js +0 -496
- package/dist/components/workflow-run-view/job-card.js.map +0 -1
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-execution-switcher.js.map +0 -1
- package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-execution-time-text.js.map +0 -1
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +0 -9
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +0 -1
- package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +0 -1
- package/src/components/workflow-run-tabs/run-jobs-list.tsx +0 -151
- package/src/components/workflow-run-tabs/run-tab-count.tsx +0 -28
- package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +0 -52
- package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +0 -117
- package/src/components/workflow-run-tabs/run-tab-strip.tsx +0 -97
- package/src/components/workflow-run-view/job-card.stories.tsx +0 -789
- package/src/components/workflow-run-view/job-card.test.tsx +0 -115
- package/src/components/workflow-run-view/job-card.tsx +0 -614
- /package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.js +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.js +0 -0
- /package/src/components/{workflow-run-view → job-detail}/job-execution-switcher.tsx +0 -0
- /package/src/components/{workflow-run-view → job-detail}/job-execution-time-text.tsx +0 -0
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import type {WorkflowRunDetailResponseDto} from '@shipfox/api-workflows-dto';
|
|
2
2
|
import {configureApiClient} from '@shipfox/client-api';
|
|
3
|
-
import {toast} from '@shipfox/react-ui/toast';
|
|
4
3
|
import {screen, waitFor, within} from '@testing-library/react';
|
|
5
4
|
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import {workflowRunsQueryKeys} from '#hooks/api/workflow-runs.js';
|
|
7
|
-
import {inlineLogBody, outputLine} from '#test/fixtures/logs.js';
|
|
8
5
|
import {
|
|
9
|
-
runAttemptsResponseDto,
|
|
10
6
|
workflowJobDto,
|
|
11
|
-
workflowJobExecutionDto,
|
|
12
|
-
workflowRunAttemptDto,
|
|
13
7
|
workflowRunDetailDto,
|
|
14
|
-
workflowRunDto,
|
|
15
|
-
workflowStepAttemptDto,
|
|
16
8
|
workflowStepDto,
|
|
17
9
|
} from '#test/fixtures/workflow-run.js';
|
|
18
10
|
import {jsonResponse, PROJECT_TEST_WSLUG, renderProjectPage} from '#test/pages.js';
|
|
@@ -20,700 +12,113 @@ import {WorkflowRunView} from './workflow-run-view.js';
|
|
|
20
12
|
|
|
21
13
|
const RUN_ID = '66666666-6666-4666-8666-666666666666';
|
|
22
14
|
const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
|
|
23
|
-
const DEFINITION_ID = '55555555-5555-4555-8555-555555555555';
|
|
24
15
|
const BUILD_JOB_ID = '77777777-7777-4777-8777-777777777777';
|
|
25
16
|
const DEPLOY_JOB_ID = '88888888-8888-4888-8888-888888888888';
|
|
26
|
-
const CHECKOUT_STEP_ID = '99999999-9999-4999-8999-999999999999';
|
|
27
|
-
const CHECKOUT_ATTEMPT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
28
|
-
const RERUN_BUTTON_NAME = /^Re-run/;
|
|
29
|
-
const BUILD_JOB_BUTTON_NAME = 'build, Succeeded';
|
|
30
|
-
const DEPLOY_JOB_BUTTON_NAME = 'deploy, Running';
|
|
31
|
-
const ATTEMPT_2_PATTERN = /Attempt 2/;
|
|
32
|
-
const COPY_RUN_BUTTON_NAME = /Copy run/;
|
|
33
|
-
const EXECUTION_ONE_MENU_ITEM_PATTERN = /#1/;
|
|
34
|
-
const LISTENING_EXECUTION_SWITCHER_NAME =
|
|
35
|
-
'Switch job execution, currently execution 2: Deployment status event';
|
|
36
|
-
const JOB_TEMPLATE_NAME = ['Implement $', '{{ event.issue.identifier }}'].join('');
|
|
37
17
|
|
|
38
18
|
describe('WorkflowRunView', () => {
|
|
39
|
-
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('reserves the shared run workspace while the run is loading', async () => {
|
|
40
24
|
configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
|
|
41
25
|
|
|
42
|
-
renderView(
|
|
26
|
+
renderView();
|
|
43
27
|
|
|
44
|
-
expect(await screen.findByRole('
|
|
45
|
-
expect(screen.
|
|
46
|
-
expect(screen.getByRole('tab', {name: 'Annotations'})).toBeInTheDocument();
|
|
47
|
-
expect(screen.getByRole('tab', {name: 'Source'})).toBeInTheDocument();
|
|
28
|
+
expect(await screen.findByRole('region', {name: 'Loading workflow run'})).toBeInTheDocument();
|
|
29
|
+
expect(screen.getByLabelText('Loading run navigation')).toBeInTheDocument();
|
|
48
30
|
expect(screen.getByRole('region', {name: 'Loading workflow run content'})).toBeInTheDocument();
|
|
31
|
+
expect(screen.queryByRole('tab', {name: 'Jobs'})).not.toBeInTheDocument();
|
|
49
32
|
});
|
|
50
33
|
|
|
51
|
-
test('
|
|
34
|
+
test('opens the all-jobs Summary on the dependency graph by default', async () => {
|
|
52
35
|
configureApiClient({
|
|
53
36
|
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
54
37
|
});
|
|
55
38
|
|
|
56
|
-
renderView();
|
|
39
|
+
const {container} = renderView();
|
|
57
40
|
|
|
58
41
|
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
59
|
-
|
|
60
42
|
expect(within(summary).getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
|
|
61
|
-
expect(
|
|
62
|
-
expect(within(summary).getByText('fire')).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByRole('navigation', {name: 'Run workspace'})).toBeInTheDocument();
|
|
63
44
|
expect(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(screen.getByRole('
|
|
69
|
-
expect(screen.getByRole('
|
|
70
|
-
|
|
71
|
-
|
|
45
|
+
screen
|
|
46
|
+
.getByRole('navigation', {name: 'Run workspace'})
|
|
47
|
+
.closest('[data-run-workspace-layout]'),
|
|
48
|
+
).toHaveClass('border-t', 'border-border-neutral-base', 'min-[768px]:flex-row');
|
|
49
|
+
expect(screen.getByRole('link', {name: 'Summary'})).toHaveAttribute('aria-current', 'page');
|
|
50
|
+
expect(screen.getByRole('heading', {name: 'Jobs'})).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByRole('heading', {name: 'Run details'})).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByRole('region', {name: 'All jobs summary'})).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByRole('region', {name: 'Workflow jobs'})).toBeInTheDocument();
|
|
54
|
+
expect(container.querySelector('[data-run-workspace-content]')).toHaveClass(
|
|
55
|
+
'bg-background-neutral-base',
|
|
56
|
+
'flex-1',
|
|
72
57
|
);
|
|
73
|
-
expect(screen.
|
|
74
|
-
expect(screen.getByRole('region', {name: 'build'})).toBeInTheDocument();
|
|
75
|
-
expect(
|
|
76
|
-
screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}),
|
|
77
|
-
).toBeInTheDocument();
|
|
58
|
+
expect(screen.queryByRole('tab', {name: 'Jobs'})).not.toBeInTheDocument();
|
|
78
59
|
});
|
|
79
60
|
|
|
80
|
-
test('
|
|
61
|
+
test('treats the removed Jobs tab URL as the graph Summary', async () => {
|
|
81
62
|
configureApiClient({
|
|
82
|
-
fetchImpl: vi.fn(() =>
|
|
83
|
-
Promise.resolve(
|
|
84
|
-
jsonResponse(
|
|
85
|
-
workflowRunViewDetailDto({
|
|
86
|
-
jobs: [
|
|
87
|
-
workflowJobDto({
|
|
88
|
-
id: BUILD_JOB_ID,
|
|
89
|
-
run_attempt_id: RUN_ID,
|
|
90
|
-
name: JOB_TEMPLATE_NAME,
|
|
91
|
-
job_executions: [
|
|
92
|
-
workflowJobExecutionDto({
|
|
93
|
-
job_id: BUILD_JOB_ID,
|
|
94
|
-
name: 'Implement ENG-123',
|
|
95
|
-
}),
|
|
96
|
-
],
|
|
97
|
-
}),
|
|
98
|
-
],
|
|
99
|
-
}),
|
|
100
|
-
),
|
|
101
|
-
),
|
|
102
|
-
),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
renderView();
|
|
106
|
-
|
|
107
|
-
expect(await screen.findByRole('region', {name: 'Implement ENG-123'})).toBeInTheDocument();
|
|
108
|
-
expect(screen.queryByRole('region', {name: JOB_TEMPLATE_NAME})).not.toBeInTheDocument();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('renders active step attempt logs inline when the selected job is running', async () => {
|
|
112
|
-
const stepId = '99999999-9999-4999-8999-000000000001';
|
|
113
|
-
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
114
|
-
const url = requestUrl(input);
|
|
115
|
-
if (url.pathname === `/steps/${stepId}/attempts/1/logs`) {
|
|
116
|
-
return Promise.resolve(jsonResponse(inlineLogBody(outputLine('live output\n'), 1)));
|
|
117
|
-
}
|
|
118
|
-
return Promise.resolve(
|
|
119
|
-
jsonResponse(
|
|
120
|
-
workflowRunViewDetailDto({
|
|
121
|
-
jobs: [
|
|
122
|
-
workflowJobDto({
|
|
123
|
-
id: BUILD_JOB_ID,
|
|
124
|
-
run_attempt_id: RUN_ID,
|
|
125
|
-
name: 'build',
|
|
126
|
-
status: 'running',
|
|
127
|
-
steps: [
|
|
128
|
-
workflowStepDto({
|
|
129
|
-
id: stepId,
|
|
130
|
-
key: 'test',
|
|
131
|
-
name: 'test',
|
|
132
|
-
status: 'running',
|
|
133
|
-
attempts: [
|
|
134
|
-
workflowStepAttemptDto({
|
|
135
|
-
id: 'aaaaaaaa-aaaa-4aaa-8aaa-000000000001',
|
|
136
|
-
step_id: stepId,
|
|
137
|
-
status: 'running',
|
|
138
|
-
exit_code: null,
|
|
139
|
-
finished_at: null,
|
|
140
|
-
}),
|
|
141
|
-
],
|
|
142
|
-
}),
|
|
143
|
-
],
|
|
144
|
-
}),
|
|
145
|
-
],
|
|
146
|
-
}),
|
|
147
|
-
),
|
|
148
|
-
);
|
|
63
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
149
64
|
});
|
|
150
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
151
65
|
|
|
152
|
-
renderView();
|
|
66
|
+
renderView({tab: 'jobs'});
|
|
153
67
|
|
|
154
|
-
expect(await screen.
|
|
155
|
-
|
|
156
|
-
.map((call) => new URL((call[0] as Request).url))
|
|
157
|
-
.find((url) => url.pathname === `/steps/${stepId}/attempts/1/logs`);
|
|
158
|
-
expect(logRequest?.searchParams.get('cursor')).toBe('0');
|
|
68
|
+
expect(await screen.findByRole('region', {name: 'All jobs summary'})).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByRole('link', {name: 'Summary'})).toHaveAttribute('aria-current', 'page');
|
|
159
70
|
});
|
|
160
71
|
|
|
161
|
-
test('
|
|
72
|
+
test('navigates graph and rail jobs to their dedicated job routes', async () => {
|
|
162
73
|
const user = userEvent.setup();
|
|
163
74
|
configureApiClient({
|
|
164
|
-
fetchImpl: vi.fn(() =>
|
|
165
|
-
Promise.resolve(
|
|
166
|
-
jsonResponse(
|
|
167
|
-
workflowRunViewDetailDto({
|
|
168
|
-
jobs: [
|
|
169
|
-
workflowJobDto({
|
|
170
|
-
id: BUILD_JOB_ID,
|
|
171
|
-
run_attempt_id: RUN_ID,
|
|
172
|
-
name: 'release-gates',
|
|
173
|
-
mode: 'listening',
|
|
174
|
-
status: 'running',
|
|
175
|
-
listener_status: 'listening',
|
|
176
|
-
job_executions: [
|
|
177
|
-
workflowJobExecutionDto({
|
|
178
|
-
id: '77777777-7777-4777-8777-000000000001',
|
|
179
|
-
job_id: BUILD_JOB_ID,
|
|
180
|
-
sequence: 1,
|
|
181
|
-
name: 'Pull request event',
|
|
182
|
-
status: 'succeeded',
|
|
183
|
-
trigger_events: [
|
|
184
|
-
{
|
|
185
|
-
source: 'github',
|
|
186
|
-
event: 'pull_request',
|
|
187
|
-
delivery_id: 'delivery-1',
|
|
188
|
-
received_at: '2026-05-07T01:00:00.000Z',
|
|
189
|
-
project: null,
|
|
190
|
-
repository: null,
|
|
191
|
-
ref: null,
|
|
192
|
-
commit: null,
|
|
193
|
-
data: {number: 12},
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
started_at: '2026-05-07T01:01:00.000Z',
|
|
197
|
-
finished_at: '2026-05-07T01:01:20.000Z',
|
|
198
|
-
steps: [
|
|
199
|
-
workflowStepDto({
|
|
200
|
-
key: 'first-event',
|
|
201
|
-
name: 'first-event',
|
|
202
|
-
status: 'succeeded',
|
|
203
|
-
attempts: [
|
|
204
|
-
workflowStepAttemptDto({
|
|
205
|
-
status: 'succeeded',
|
|
206
|
-
finished_at: '2026-05-07T01:01:20.000Z',
|
|
207
|
-
}),
|
|
208
|
-
],
|
|
209
|
-
}),
|
|
210
|
-
],
|
|
211
|
-
}),
|
|
212
|
-
workflowJobExecutionDto({
|
|
213
|
-
id: '77777777-7777-4777-8777-000000000002',
|
|
214
|
-
job_id: BUILD_JOB_ID,
|
|
215
|
-
sequence: 2,
|
|
216
|
-
name: 'Deployment status event',
|
|
217
|
-
status: 'failed',
|
|
218
|
-
status_reason: 'step_failed',
|
|
219
|
-
trigger_events: [
|
|
220
|
-
{
|
|
221
|
-
source: 'github',
|
|
222
|
-
event: 'deployment_status',
|
|
223
|
-
delivery_id: 'delivery-2',
|
|
224
|
-
received_at: '2026-05-07T01:01:00.000Z',
|
|
225
|
-
project: null,
|
|
226
|
-
repository: null,
|
|
227
|
-
ref: null,
|
|
228
|
-
commit: null,
|
|
229
|
-
data: {state: 'success'},
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
source: 'github',
|
|
233
|
-
event: 'check_run',
|
|
234
|
-
delivery_id: 'delivery-3',
|
|
235
|
-
received_at: '2026-05-07T01:01:10.000Z',
|
|
236
|
-
project: null,
|
|
237
|
-
repository: null,
|
|
238
|
-
ref: null,
|
|
239
|
-
commit: null,
|
|
240
|
-
data: {status: 'completed'},
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
started_at: '2026-05-07T01:02:00.000Z',
|
|
244
|
-
finished_at: '2026-05-07T01:02:20.000Z',
|
|
245
|
-
steps: [
|
|
246
|
-
workflowStepDto({
|
|
247
|
-
key: 'second-event',
|
|
248
|
-
name: 'second-event',
|
|
249
|
-
status: 'failed',
|
|
250
|
-
attempts: [
|
|
251
|
-
workflowStepAttemptDto({
|
|
252
|
-
status: 'failed',
|
|
253
|
-
exit_code: 1,
|
|
254
|
-
finished_at: '2026-05-07T01:02:20.000Z',
|
|
255
|
-
}),
|
|
256
|
-
],
|
|
257
|
-
}),
|
|
258
|
-
],
|
|
259
|
-
}),
|
|
260
|
-
],
|
|
261
|
-
}),
|
|
262
|
-
],
|
|
263
|
-
}),
|
|
264
|
-
),
|
|
265
|
-
),
|
|
266
|
-
),
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
renderView();
|
|
270
|
-
expect(
|
|
271
|
-
await screen.findByRole('region', {name: 'Deployment status event'}),
|
|
272
|
-
).toBeInTheDocument();
|
|
273
|
-
expect(await screen.findByText('second-event')).toBeInTheDocument();
|
|
274
|
-
expect(
|
|
275
|
-
await screen.findByRole('button', {name: 'github · deployment_status (2 events)'}),
|
|
276
|
-
).toBeInTheDocument();
|
|
277
|
-
expect(screen.getByText('deployment_status (2)')).toBeInTheDocument();
|
|
278
|
-
|
|
279
|
-
await user.click(screen.getByRole('button', {name: LISTENING_EXECUTION_SWITCHER_NAME}));
|
|
280
|
-
await user.click(screen.getByRole('menuitem', {name: EXECUTION_ONE_MENU_ITEM_PATTERN}));
|
|
281
|
-
|
|
282
|
-
expect(await screen.findByText('first-event')).toBeInTheDocument();
|
|
283
|
-
expect(screen.getByRole('region', {name: 'Pull request event'})).toBeInTheDocument();
|
|
284
|
-
expect(screen.queryByText('second-event')).not.toBeInTheDocument();
|
|
285
|
-
expect(await screen.findByRole('button', {name: 'github · pull_request'})).toBeInTheDocument();
|
|
286
|
-
expect(screen.getByText('pull_request')).toBeInTheDocument();
|
|
287
|
-
expect(screen.queryByText('deployment_status (2)')).not.toBeInTheDocument();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test('shows generic provider setup guidance for agent config failures', async () => {
|
|
291
|
-
const user = userEvent.setup();
|
|
292
|
-
const stepId = '99999999-9999-4999-8999-000000000004';
|
|
293
|
-
const attemptId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000004';
|
|
294
|
-
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
295
|
-
const url = requestUrl(input);
|
|
296
|
-
if (url.pathname === `/steps/${stepId}/attempts/1/logs`) {
|
|
297
|
-
return Promise.resolve(
|
|
298
|
-
jsonResponse(inlineLogBody(outputLine('configuration failed\n'), 1)),
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
return Promise.resolve(
|
|
302
|
-
jsonResponse(
|
|
303
|
-
workflowRunViewDetailDto({
|
|
304
|
-
jobs: [
|
|
305
|
-
workflowJobDto({
|
|
306
|
-
id: BUILD_JOB_ID,
|
|
307
|
-
run_attempt_id: RUN_ID,
|
|
308
|
-
name: 'build',
|
|
309
|
-
status: 'failed',
|
|
310
|
-
steps: [
|
|
311
|
-
workflowStepDto({
|
|
312
|
-
id: stepId,
|
|
313
|
-
key: 'implement',
|
|
314
|
-
name: 'Fix the failing tests.',
|
|
315
|
-
type: 'agent',
|
|
316
|
-
status: 'failed',
|
|
317
|
-
config: {
|
|
318
|
-
provider: 'anthropic',
|
|
319
|
-
model: 'claude-opus-4-8',
|
|
320
|
-
thinking: 'high',
|
|
321
|
-
prompt: 'Fix the failing tests.',
|
|
322
|
-
},
|
|
323
|
-
error: {
|
|
324
|
-
message: 'Model provider request failed',
|
|
325
|
-
reason: 'agent_invocation_failed',
|
|
326
|
-
category: 'user',
|
|
327
|
-
},
|
|
328
|
-
attempts: [
|
|
329
|
-
workflowStepAttemptDto({
|
|
330
|
-
id: attemptId,
|
|
331
|
-
step_id: stepId,
|
|
332
|
-
status: 'failed',
|
|
333
|
-
exit_code: 1,
|
|
334
|
-
error: {
|
|
335
|
-
message: 'Model provider credentials are not configured',
|
|
336
|
-
reason: 'agent_config_invalid',
|
|
337
|
-
agentConfigIssue: 'provider_not_configured',
|
|
338
|
-
},
|
|
339
|
-
finished_at: '2026-05-07T01:01:20.000Z',
|
|
340
|
-
}),
|
|
341
|
-
],
|
|
342
|
-
}),
|
|
343
|
-
],
|
|
344
|
-
}),
|
|
345
|
-
],
|
|
346
|
-
}),
|
|
347
|
-
),
|
|
348
|
-
);
|
|
75
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
349
76
|
});
|
|
350
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
351
77
|
|
|
352
|
-
renderView();
|
|
353
|
-
await user.click(
|
|
354
|
-
await screen.findByRole('button', {
|
|
355
|
-
name: 'Fix the failing tests., Failed, attempt 1, User',
|
|
356
|
-
}),
|
|
357
|
-
);
|
|
78
|
+
const {router} = renderView();
|
|
79
|
+
await user.click(await screen.findByRole('button', {name: 'deploy, Running'}));
|
|
358
80
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
expect(screen.queryByText('anthropic')).not.toBeInTheDocument();
|
|
363
|
-
expect(screen.queryByText('claude-opus-4-8')).not.toBeInTheDocument();
|
|
364
|
-
expect(screen.queryByText('high')).not.toBeInTheDocument();
|
|
365
|
-
expect(screen.getByText('Configure credentials for anthropic')).toBeInTheDocument();
|
|
366
|
-
expect(
|
|
367
|
-
screen.getByText(
|
|
368
|
-
'This step uses anthropic, but no workspace credentials are configured for that model provider. Configure anthropic in Agents, then re-run the workflow.',
|
|
81
|
+
await waitFor(() =>
|
|
82
|
+
expect(router.state.location.pathname).toBe(
|
|
83
|
+
`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}/jobs/${DEPLOY_JOB_ID}`,
|
|
369
84
|
),
|
|
370
|
-
).toBeInTheDocument();
|
|
371
|
-
expect(screen.getByRole('link', {name: 'Configure Agents'})).toHaveAttribute(
|
|
372
|
-
'href',
|
|
373
|
-
`/w/${PROJECT_TEST_WSLUG}/settings/agents`,
|
|
374
85
|
);
|
|
375
86
|
});
|
|
376
87
|
|
|
377
|
-
test('
|
|
88
|
+
test('keeps run Annotations and Source in the workspace navigation', async () => {
|
|
378
89
|
const user = userEvent.setup();
|
|
379
|
-
const stepId = '99999999-9999-4999-8999-000000000005';
|
|
380
|
-
const attemptId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000005';
|
|
381
90
|
configureApiClient({
|
|
382
|
-
fetchImpl: vi.fn((
|
|
383
|
-
const url = requestUrl(input);
|
|
384
|
-
if (url.pathname === `/steps/${stepId}/attempts/1/logs`) {
|
|
385
|
-
return Promise.resolve(
|
|
386
|
-
jsonResponse(inlineLogBody(outputLine('configuration failed\n'), 1)),
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
return Promise.resolve(
|
|
390
|
-
jsonResponse(
|
|
391
|
-
workflowRunViewDetailDto({
|
|
392
|
-
jobs: [
|
|
393
|
-
workflowJobDto({
|
|
394
|
-
id: BUILD_JOB_ID,
|
|
395
|
-
run_attempt_id: RUN_ID,
|
|
396
|
-
name: 'build',
|
|
397
|
-
status: 'failed',
|
|
398
|
-
steps: [
|
|
399
|
-
workflowStepDto({
|
|
400
|
-
id: stepId,
|
|
401
|
-
key: 'implement',
|
|
402
|
-
name: 'Fix the failing tests.',
|
|
403
|
-
type: 'agent',
|
|
404
|
-
status: 'failed',
|
|
405
|
-
config: {
|
|
406
|
-
provider: 'anthropic',
|
|
407
|
-
model: 'claude-opus-4-8',
|
|
408
|
-
thinking: 'high',
|
|
409
|
-
prompt: 'Fix the failing tests.',
|
|
410
|
-
},
|
|
411
|
-
error: {
|
|
412
|
-
message: 'Model provider credentials are not configured',
|
|
413
|
-
reason: 'agent_config_invalid',
|
|
414
|
-
agent_config_issue: 'provider_not_configured',
|
|
415
|
-
category: 'user',
|
|
416
|
-
},
|
|
417
|
-
attempts: [
|
|
418
|
-
workflowStepAttemptDto({
|
|
419
|
-
id: attemptId,
|
|
420
|
-
step_id: stepId,
|
|
421
|
-
status: 'failed',
|
|
422
|
-
exit_code: 1,
|
|
423
|
-
error: null,
|
|
424
|
-
finished_at: '2026-05-07T01:01:20.000Z',
|
|
425
|
-
}),
|
|
426
|
-
],
|
|
427
|
-
}),
|
|
428
|
-
],
|
|
429
|
-
}),
|
|
430
|
-
],
|
|
431
|
-
}),
|
|
432
|
-
),
|
|
433
|
-
);
|
|
434
|
-
}),
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
renderView();
|
|
438
|
-
await user.click(
|
|
439
|
-
await screen.findByRole('button', {
|
|
440
|
-
name: 'Fix the failing tests., Failed, attempt 1, User',
|
|
441
|
-
}),
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
expect(screen.queryByText('anthropic')).not.toBeInTheDocument();
|
|
445
|
-
expect(screen.queryByText('claude-opus-4-8')).not.toBeInTheDocument();
|
|
446
|
-
expect(screen.queryByText('high')).not.toBeInTheDocument();
|
|
447
|
-
expect(screen.getByText('Configure credentials for anthropic')).toBeInTheDocument();
|
|
448
|
-
expect(screen.getByRole('link', {name: 'Configure Agents'})).toHaveAttribute(
|
|
449
|
-
'href',
|
|
450
|
-
`/w/${PROJECT_TEST_WSLUG}/settings/agents`,
|
|
451
|
-
);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
test('renders skipped zero-attempt jobs as skipped instead of missing attempts', async () => {
|
|
455
|
-
configureApiClient({
|
|
456
|
-
fetchImpl: vi.fn(() =>
|
|
457
|
-
Promise.resolve(
|
|
458
|
-
jsonResponse(
|
|
459
|
-
workflowRunViewDetailDto({
|
|
460
|
-
jobs: [
|
|
461
|
-
workflowJobDto({
|
|
462
|
-
id: DEPLOY_JOB_ID,
|
|
463
|
-
run_attempt_id: RUN_ID,
|
|
464
|
-
name: 'deploy',
|
|
465
|
-
status: 'skipped',
|
|
466
|
-
status_reason: 'dependency_not_completed',
|
|
467
|
-
job_executions: [],
|
|
468
|
-
}),
|
|
469
|
-
],
|
|
470
|
-
}),
|
|
471
|
-
),
|
|
472
|
-
),
|
|
473
|
-
),
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
renderView();
|
|
477
|
-
|
|
478
|
-
expect(await screen.findByText('This job was skipped')).toBeInTheDocument();
|
|
479
|
-
expect(
|
|
480
|
-
screen.getByText('A required job did not complete, so this job was skipped.'),
|
|
481
|
-
).toBeInTheDocument();
|
|
482
|
-
expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
test('keeps a terminal job status over inconsistent zero-step execution activity', async () => {
|
|
486
|
-
configureApiClient({
|
|
487
|
-
fetchImpl: vi.fn(() =>
|
|
488
|
-
Promise.resolve(
|
|
489
|
-
jsonResponse(
|
|
490
|
-
workflowRunViewDetailDto({
|
|
491
|
-
jobs: [
|
|
492
|
-
workflowJobDto({
|
|
493
|
-
id: DEPLOY_JOB_ID,
|
|
494
|
-
run_attempt_id: RUN_ID,
|
|
495
|
-
name: 'deploy',
|
|
496
|
-
status: 'succeeded',
|
|
497
|
-
job_executions: [
|
|
498
|
-
workflowJobExecutionDto({
|
|
499
|
-
id: '77777777-7777-4777-8777-000000000003',
|
|
500
|
-
job_id: DEPLOY_JOB_ID,
|
|
501
|
-
sequence: 2,
|
|
502
|
-
name: 'retry',
|
|
503
|
-
status: 'running',
|
|
504
|
-
steps: [],
|
|
505
|
-
}),
|
|
506
|
-
],
|
|
507
|
-
}),
|
|
508
|
-
],
|
|
509
|
-
}),
|
|
510
|
-
),
|
|
511
|
-
),
|
|
512
|
-
),
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
renderView();
|
|
516
|
-
|
|
517
|
-
expect(await screen.findByText('No steps recorded')).toBeInTheDocument();
|
|
518
|
-
expect(screen.getByText('Execution #2 finished without recorded steps.')).toBeInTheDocument();
|
|
519
|
-
expect(screen.queryByText('Waiting for the first step')).not.toBeInTheDocument();
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
test('renders pending zero-step jobs as waiting to start', async () => {
|
|
523
|
-
configureApiClient({
|
|
524
|
-
fetchImpl: vi.fn(() =>
|
|
525
|
-
Promise.resolve(
|
|
526
|
-
jsonResponse(
|
|
527
|
-
workflowRunViewDetailDto({
|
|
528
|
-
jobs: [
|
|
529
|
-
workflowJobDto({
|
|
530
|
-
id: DEPLOY_JOB_ID,
|
|
531
|
-
run_attempt_id: RUN_ID,
|
|
532
|
-
name: 'deploy',
|
|
533
|
-
status: 'pending',
|
|
534
|
-
steps: [],
|
|
535
|
-
}),
|
|
536
|
-
],
|
|
537
|
-
}),
|
|
538
|
-
),
|
|
539
|
-
),
|
|
540
|
-
),
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
renderView();
|
|
544
|
-
|
|
545
|
-
expect(await screen.findByText('Waiting for this job to start')).toBeInTheDocument();
|
|
546
|
-
expect(screen.getByText('Steps will appear here once the job starts.')).toBeInTheDocument();
|
|
547
|
-
expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
test('renders claimed idle jobs as runner preparation', async () => {
|
|
551
|
-
configureApiClient({
|
|
552
|
-
fetchImpl: vi.fn(() =>
|
|
553
|
-
Promise.resolve(
|
|
554
|
-
jsonResponse(
|
|
555
|
-
workflowRunViewDetailDto({
|
|
556
|
-
jobs: [
|
|
557
|
-
workflowJobDto({
|
|
558
|
-
id: DEPLOY_JOB_ID,
|
|
559
|
-
run_attempt_id: RUN_ID,
|
|
560
|
-
name: 'deploy',
|
|
561
|
-
status: 'running',
|
|
562
|
-
job_executions: [
|
|
563
|
-
workflowJobExecutionDto({
|
|
564
|
-
job_id: DEPLOY_JOB_ID,
|
|
565
|
-
status: 'running',
|
|
566
|
-
steps: [],
|
|
567
|
-
}),
|
|
568
|
-
],
|
|
569
|
-
}),
|
|
570
|
-
],
|
|
571
|
-
}),
|
|
572
|
-
),
|
|
573
|
-
),
|
|
574
|
-
),
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
renderView();
|
|
578
|
-
|
|
579
|
-
expect(await screen.findByText('Runner preparing job')).toBeInTheDocument();
|
|
580
|
-
expect(
|
|
581
|
-
screen.getByText('A runner is assigned. Steps will appear here when work begins.'),
|
|
582
|
-
).toBeInTheDocument();
|
|
583
|
-
expect(
|
|
584
|
-
screen.queryByText('This job is running, but no steps have started yet.'),
|
|
585
|
-
).not.toBeInTheDocument();
|
|
586
|
-
expect(screen.queryByText('No steps recorded')).not.toBeInTheDocument();
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test('renders active listening jobs with no executions as waiting for trigger events', async () => {
|
|
590
|
-
configureApiClient({
|
|
591
|
-
fetchImpl: vi.fn(() =>
|
|
592
|
-
Promise.resolve(
|
|
593
|
-
jsonResponse(
|
|
594
|
-
workflowRunViewDetailDto({
|
|
595
|
-
jobs: [
|
|
596
|
-
workflowJobDto({
|
|
597
|
-
id: DEPLOY_JOB_ID,
|
|
598
|
-
run_attempt_id: RUN_ID,
|
|
599
|
-
name: 'release-gates',
|
|
600
|
-
mode: 'listening',
|
|
601
|
-
status: 'running',
|
|
602
|
-
listener_status: 'listening',
|
|
603
|
-
job_executions: [],
|
|
604
|
-
}),
|
|
605
|
-
],
|
|
606
|
-
}),
|
|
607
|
-
),
|
|
608
|
-
),
|
|
609
|
-
),
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
renderView();
|
|
613
|
-
|
|
614
|
-
expect(await screen.findByText('Waiting for trigger events')).toBeInTheDocument();
|
|
615
|
-
expect(
|
|
616
|
-
screen.getByText('Matching trigger events will create job executions here.'),
|
|
617
|
-
).toBeInTheDocument();
|
|
618
|
-
expect(screen.queryByText('Execution details unavailable')).not.toBeInTheDocument();
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test('renders finished one-shot jobs with no execution as unavailable execution details', async () => {
|
|
622
|
-
configureApiClient({
|
|
623
|
-
fetchImpl: vi.fn(() =>
|
|
624
|
-
Promise.resolve(
|
|
625
|
-
jsonResponse(
|
|
626
|
-
workflowRunViewDetailDto({
|
|
627
|
-
jobs: [
|
|
628
|
-
workflowJobDto({
|
|
629
|
-
id: DEPLOY_JOB_ID,
|
|
630
|
-
run_attempt_id: RUN_ID,
|
|
631
|
-
name: 'deploy',
|
|
632
|
-
status: 'succeeded',
|
|
633
|
-
job_executions: [],
|
|
634
|
-
}),
|
|
635
|
-
],
|
|
636
|
-
}),
|
|
637
|
-
),
|
|
638
|
-
),
|
|
639
|
-
),
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
renderView();
|
|
643
|
-
|
|
644
|
-
expect(await screen.findByText('Execution details unavailable')).toBeInTheDocument();
|
|
645
|
-
expect(
|
|
646
|
-
screen.getByText('This job finished, but no job execution record is available.'),
|
|
647
|
-
).toBeInTheDocument();
|
|
648
|
-
expect(screen.queryByText('Waiting for trigger events')).not.toBeInTheDocument();
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
test('renders cancelled zero-attempt jobs separately from skipped jobs', async () => {
|
|
652
|
-
configureApiClient({
|
|
653
|
-
fetchImpl: vi.fn(() =>
|
|
654
|
-
Promise.resolve(
|
|
655
|
-
jsonResponse(
|
|
656
|
-
workflowRunViewDetailDto({
|
|
657
|
-
jobs: [
|
|
658
|
-
workflowJobDto({
|
|
659
|
-
id: DEPLOY_JOB_ID,
|
|
660
|
-
run_attempt_id: RUN_ID,
|
|
661
|
-
name: 'deploy',
|
|
662
|
-
status: 'cancelled',
|
|
663
|
-
job_executions: [
|
|
664
|
-
workflowJobExecutionDto({
|
|
665
|
-
job_id: DEPLOY_JOB_ID,
|
|
666
|
-
status: 'cancelled',
|
|
667
|
-
steps: [],
|
|
668
|
-
}),
|
|
669
|
-
],
|
|
670
|
-
}),
|
|
671
|
-
],
|
|
672
|
-
}),
|
|
673
|
-
),
|
|
674
|
-
),
|
|
675
|
-
),
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
renderView();
|
|
679
|
-
|
|
680
|
-
expect(await screen.findByText('Cancelled before start')).toBeInTheDocument();
|
|
681
|
-
expect(screen.getByText('This job was cancelled before any step started.')).toBeInTheDocument();
|
|
682
|
-
expect(screen.queryByText('This job was skipped')).not.toBeInTheDocument();
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
test('shows the not-found surface when the run 404s', async () => {
|
|
686
|
-
configureApiClient({
|
|
687
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}))),
|
|
91
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
688
92
|
});
|
|
689
93
|
|
|
690
|
-
renderView();
|
|
94
|
+
const {router} = renderView();
|
|
95
|
+
await screen.findByRole('region', {name: 'Workflow jobs'});
|
|
96
|
+
await user.click(screen.getByRole('link', {name: 'Annotations'}));
|
|
691
97
|
|
|
692
|
-
|
|
98
|
+
await waitFor(() => expect(router.state.location.search).toMatchObject({tab: 'annotations'}));
|
|
693
99
|
});
|
|
694
100
|
|
|
695
|
-
test('
|
|
101
|
+
test('filters the single run-level Annotations page by job', async () => {
|
|
696
102
|
configureApiClient({
|
|
697
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(
|
|
103
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
698
104
|
});
|
|
699
105
|
|
|
700
|
-
renderView();
|
|
106
|
+
renderView({tab: 'annotations', selection: {jobId: BUILD_JOB_ID}});
|
|
701
107
|
|
|
702
108
|
expect(
|
|
703
|
-
await screen.findByRole('
|
|
704
|
-
).
|
|
109
|
+
await screen.findByRole('combobox', {name: 'Filter annotations by job'}),
|
|
110
|
+
).toHaveTextContent('build');
|
|
111
|
+
expect(screen.getByText('build has no annotations to show in this run.')).toBeInTheDocument();
|
|
112
|
+
expect(screen.queryByRole('tab', {name: 'Annotations'})).not.toBeInTheDocument();
|
|
705
113
|
});
|
|
706
114
|
|
|
707
|
-
test('renders the
|
|
115
|
+
test('renders the captured workflow source in the Source section', async () => {
|
|
708
116
|
configureApiClient({
|
|
709
117
|
fetchImpl: vi.fn(() =>
|
|
710
118
|
Promise.resolve(
|
|
711
119
|
jsonResponse(
|
|
712
120
|
workflowRunViewDetailDto({
|
|
713
|
-
source_snapshot: {
|
|
714
|
-
format: 'yaml',
|
|
715
|
-
content: 'jobs:\n build:\n steps:\n - run: pnpm test',
|
|
716
|
-
},
|
|
121
|
+
source_snapshot: {format: 'yaml', content: 'jobs:\n build:\n steps: []'},
|
|
717
122
|
}),
|
|
718
123
|
),
|
|
719
124
|
),
|
|
@@ -722,505 +127,49 @@ describe('WorkflowRunView', () => {
|
|
|
722
127
|
|
|
723
128
|
renderView({tab: 'source'});
|
|
724
129
|
|
|
725
|
-
expect(await screen.
|
|
726
|
-
|
|
727
|
-
'true',
|
|
728
|
-
);
|
|
729
|
-
expect(await screen.findByText('pnpm test')).toBeInTheDocument();
|
|
730
|
-
expect(screen.queryByRole('dialog', {name: 'Workflow source'})).not.toBeInTheDocument();
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
test('highlights selected step source lines when the source panel opens', async () => {
|
|
734
|
-
const user = userEvent.setup();
|
|
735
|
-
const stepId = '99999999-9999-4999-8999-000000000003';
|
|
736
|
-
const stepAttemptId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000003';
|
|
737
|
-
configureApiClient({
|
|
738
|
-
fetchImpl: vi.fn(() =>
|
|
739
|
-
Promise.resolve(
|
|
740
|
-
jsonResponse(
|
|
741
|
-
workflowRunViewDetailDto({
|
|
742
|
-
source_snapshot: {
|
|
743
|
-
format: 'yaml',
|
|
744
|
-
content: 'jobs:\n deploy:\n steps:\n - run: ship',
|
|
745
|
-
},
|
|
746
|
-
jobs: [
|
|
747
|
-
workflowJobDto({
|
|
748
|
-
id: DEPLOY_JOB_ID,
|
|
749
|
-
run_attempt_id: RUN_ID,
|
|
750
|
-
name: 'deploy',
|
|
751
|
-
status: 'running',
|
|
752
|
-
job_executions: [
|
|
753
|
-
workflowJobExecutionDto({
|
|
754
|
-
job_id: DEPLOY_JOB_ID,
|
|
755
|
-
name: 'deploy',
|
|
756
|
-
status: 'running',
|
|
757
|
-
steps: [
|
|
758
|
-
workflowStepDto({
|
|
759
|
-
id: stepId,
|
|
760
|
-
key: 'deploy',
|
|
761
|
-
name: 'deploy',
|
|
762
|
-
source_location: {start_line: 2, end_line: 3},
|
|
763
|
-
status: 'running',
|
|
764
|
-
attempts: [
|
|
765
|
-
workflowStepAttemptDto({
|
|
766
|
-
id: stepAttemptId,
|
|
767
|
-
step_id: stepId,
|
|
768
|
-
status: 'running',
|
|
769
|
-
}),
|
|
770
|
-
],
|
|
771
|
-
}),
|
|
772
|
-
],
|
|
773
|
-
}),
|
|
774
|
-
],
|
|
775
|
-
}),
|
|
776
|
-
],
|
|
777
|
-
}),
|
|
778
|
-
),
|
|
779
|
-
),
|
|
780
|
-
),
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
renderView({tab: 'jobs', selection: {stepId, stepAttemptId}});
|
|
784
|
-
const jobRegion = await screen.findByRole('region', {name: 'deploy'});
|
|
785
|
-
await user.click(within(jobRegion).getByRole('button', {name: 'View source'}));
|
|
786
|
-
|
|
787
|
-
const sourceDialog = await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
788
|
-
const highlightedLines = sourceDialog.querySelectorAll('.line.highlighted-line');
|
|
789
|
-
expect(highlightedLines).toHaveLength(2);
|
|
790
|
-
expect(highlightedLines[0]).toHaveTextContent('deploy:');
|
|
791
|
-
expect(highlightedLines[1]).toHaveTextContent('steps:');
|
|
130
|
+
expect(await screen.findByText('build:')).toBeInTheDocument();
|
|
131
|
+
expect(screen.getByRole('link', {name: 'Source'})).toHaveAttribute('aria-current', 'page');
|
|
792
132
|
});
|
|
793
133
|
|
|
794
134
|
test('explains when the run has no source snapshot', async () => {
|
|
795
135
|
configureApiClient({
|
|
796
|
-
fetchImpl: vi.fn(() =>
|
|
797
|
-
Promise.resolve(jsonResponse(workflowRunViewDetailDto({source_snapshot: null}))),
|
|
798
|
-
),
|
|
136
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
|
|
799
137
|
});
|
|
800
138
|
|
|
801
139
|
renderView({tab: 'source'});
|
|
802
140
|
|
|
803
|
-
await screen.findByRole('region', {name: 'deploy-web'});
|
|
804
|
-
|
|
805
141
|
expect(await screen.findByText('Source snapshot unavailable')).toBeInTheDocument();
|
|
806
142
|
});
|
|
807
143
|
|
|
808
|
-
test('
|
|
809
|
-
const user = userEvent.setup();
|
|
810
|
-
configureApiClient({
|
|
811
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(locatedStepSourceDetail()))),
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
renderView();
|
|
815
|
-
|
|
816
|
-
const jobRegion = await screen.findByRole('region', {name: 'build'});
|
|
817
|
-
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
818
|
-
const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
|
|
819
|
-
expect(jobSourceButton).toHaveAttribute('aria-expanded', 'false');
|
|
820
|
-
expect(jobSourceButton).toHaveAttribute('aria-controls');
|
|
821
|
-
|
|
822
|
-
await user.click(jobSourceButton);
|
|
823
|
-
|
|
824
|
-
const sourceDialog = await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
825
|
-
const highlighted = sourceDialog.querySelectorAll('.line.highlighted-line');
|
|
826
|
-
expect(highlighted).toHaveLength(2);
|
|
827
|
-
expect(highlighted[0]).toHaveTextContent('build:');
|
|
828
|
-
expect(highlighted[1]).toHaveTextContent('steps:');
|
|
829
|
-
expect(jobSourceButton).toHaveAttribute('aria-expanded', 'true');
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
test('returns focus to the job Source button on close and preserves the selected job', async () => {
|
|
833
|
-
const user = userEvent.setup();
|
|
834
|
-
configureApiClient({
|
|
835
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(locatedStepSourceDetail()))),
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
renderView();
|
|
839
|
-
|
|
840
|
-
const jobRegion = await screen.findByRole('region', {name: 'build'});
|
|
841
|
-
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
842
|
-
const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
|
|
843
|
-
|
|
844
|
-
await user.click(jobSourceButton);
|
|
845
|
-
await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
846
|
-
await user.click(screen.getByRole('button', {name: 'Close source'}));
|
|
847
|
-
|
|
848
|
-
await waitFor(() => expect(jobSourceButton).toHaveFocus());
|
|
849
|
-
expect(jobSourceButton).toHaveAttribute('aria-expanded', 'false');
|
|
850
|
-
expect(screen.getByRole('region', {name: 'build'})).toBeInTheDocument();
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
test('closes the source panel when refetch removes the focused step source', async () => {
|
|
854
|
-
const user = userEvent.setup();
|
|
855
|
-
let detailRequests = 0;
|
|
856
|
-
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
857
|
-
const url = requestUrl(input);
|
|
858
|
-
if (url.pathname === `/workflows/runs/${RUN_ID}`) {
|
|
859
|
-
detailRequests += 1;
|
|
860
|
-
return Promise.resolve(
|
|
861
|
-
jsonResponse(
|
|
862
|
-
detailRequests === 1
|
|
863
|
-
? locatedStepSourceDetail()
|
|
864
|
-
: locatedStepSourceDetail({
|
|
865
|
-
jobs: [
|
|
866
|
-
workflowJobDto({
|
|
867
|
-
id: BUILD_JOB_ID,
|
|
868
|
-
name: 'build',
|
|
869
|
-
status: 'succeeded',
|
|
870
|
-
steps: [
|
|
871
|
-
workflowStepDto({
|
|
872
|
-
id: CHECKOUT_STEP_ID,
|
|
873
|
-
name: 'checkout',
|
|
874
|
-
status: 'succeeded',
|
|
875
|
-
source_location: null,
|
|
876
|
-
attempts: [
|
|
877
|
-
workflowStepAttemptDto({
|
|
878
|
-
id: CHECKOUT_ATTEMPT_ID,
|
|
879
|
-
step_id: CHECKOUT_STEP_ID,
|
|
880
|
-
status: 'succeeded',
|
|
881
|
-
exit_code: 0,
|
|
882
|
-
finished_at: '2026-05-07T01:01:20.000Z',
|
|
883
|
-
}),
|
|
884
|
-
],
|
|
885
|
-
}),
|
|
886
|
-
],
|
|
887
|
-
}),
|
|
888
|
-
],
|
|
889
|
-
}),
|
|
890
|
-
),
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
return Promise.resolve(jsonResponse(inlineLogBody(outputLine('done\n'), 1)));
|
|
894
|
-
});
|
|
895
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
896
|
-
|
|
897
|
-
const {queryClient} = renderView();
|
|
898
|
-
|
|
899
|
-
const jobRegion = await screen.findByRole('region', {name: 'build'});
|
|
900
|
-
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
901
|
-
const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
|
|
902
|
-
await user.click(jobSourceButton);
|
|
903
|
-
await screen.findByRole('dialog', {name: 'Workflow source'});
|
|
904
|
-
|
|
905
|
-
await queryClient.refetchQueries({queryKey: workflowRunsQueryKeys.detail(RUN_ID)});
|
|
906
|
-
await waitFor(() =>
|
|
907
|
-
expect(
|
|
908
|
-
within(jobRegion).queryByRole('button', {name: 'View source'}),
|
|
909
|
-
).not.toBeInTheDocument(),
|
|
910
|
-
);
|
|
911
|
-
expect(screen.queryByRole('dialog', {name: 'Workflow source'})).not.toBeInTheDocument();
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
test('re-runs all jobs from a succeeded run and navigates to the new run', async () => {
|
|
915
|
-
const user = userEvent.setup();
|
|
916
|
-
const rerunId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
|
917
|
-
const successSpy = vi.spyOn(toast, 'success').mockImplementation(() => 'toast-id');
|
|
918
|
-
const postBodies: unknown[] = [];
|
|
919
|
-
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
920
|
-
const request = input as Request;
|
|
921
|
-
const url = requestUrl(input);
|
|
922
|
-
if (request.method === 'POST' && url.pathname === `/workflows/runs/${RUN_ID}/rerun`) {
|
|
923
|
-
postBodies.push(await request.clone().json());
|
|
924
|
-
return jsonResponse(workflowRunDto({id: rerunId, status: 'pending'}));
|
|
925
|
-
}
|
|
926
|
-
return jsonResponse(
|
|
927
|
-
workflowRunViewDetailDto({
|
|
928
|
-
status: 'succeeded',
|
|
929
|
-
jobs: [
|
|
930
|
-
workflowJobDto({
|
|
931
|
-
id: BUILD_JOB_ID,
|
|
932
|
-
run_attempt_id: RUN_ID,
|
|
933
|
-
name: 'build',
|
|
934
|
-
status: 'succeeded',
|
|
935
|
-
}),
|
|
936
|
-
],
|
|
937
|
-
}),
|
|
938
|
-
);
|
|
939
|
-
});
|
|
940
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
941
|
-
|
|
942
|
-
const {router} = renderView({}, '?runAttempt=1');
|
|
943
|
-
await user.click(await screen.findByRole('button', {name: 'Re-run workflow'}));
|
|
944
|
-
|
|
945
|
-
const postRequest = await findRequest(fetchImpl, 'POST', `/workflows/runs/${RUN_ID}/rerun`);
|
|
946
|
-
expect(postRequest).toBeDefined();
|
|
947
|
-
expect(postBodies).toEqual([{mode: 'all'}]);
|
|
948
|
-
expect(successSpy).toHaveBeenCalledWith('Re-run started');
|
|
949
|
-
await waitFor(() =>
|
|
950
|
-
expect(router.state.location.pathname).toBe(
|
|
951
|
-
`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${rerunId}`,
|
|
952
|
-
),
|
|
953
|
-
);
|
|
954
|
-
expect((router.state.location.search as Record<string, unknown>).runAttempt).toBeUndefined();
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
test('re-runs failed jobs from the dropdown', async () => {
|
|
958
|
-
const user = userEvent.setup();
|
|
959
|
-
const postBodies: unknown[] = [];
|
|
960
|
-
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
961
|
-
const request = input as Request;
|
|
962
|
-
const url = requestUrl(input);
|
|
963
|
-
if (request.method === 'POST' && url.pathname === `/workflows/runs/${RUN_ID}/rerun`) {
|
|
964
|
-
postBodies.push(await request.clone().json());
|
|
965
|
-
return jsonResponse(workflowRunDto({id: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'}));
|
|
966
|
-
}
|
|
967
|
-
return jsonResponse(
|
|
968
|
-
workflowRunViewDetailDto({
|
|
969
|
-
status: 'failed',
|
|
970
|
-
jobs: [
|
|
971
|
-
workflowJobDto({
|
|
972
|
-
id: BUILD_JOB_ID,
|
|
973
|
-
run_attempt_id: RUN_ID,
|
|
974
|
-
name: 'build',
|
|
975
|
-
status: 'failed',
|
|
976
|
-
}),
|
|
977
|
-
workflowJobDto({
|
|
978
|
-
id: DEPLOY_JOB_ID,
|
|
979
|
-
run_attempt_id: RUN_ID,
|
|
980
|
-
name: 'deploy',
|
|
981
|
-
status: 'cancelled',
|
|
982
|
-
position: 1,
|
|
983
|
-
}),
|
|
984
|
-
],
|
|
985
|
-
}),
|
|
986
|
-
);
|
|
987
|
-
});
|
|
988
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
989
|
-
|
|
990
|
-
renderView();
|
|
991
|
-
await user.click(await screen.findByRole('button', {name: 'Re-run jobs'}));
|
|
992
|
-
await user.click(await screen.findByRole('menuitem', {name: 'Re-run failed jobs'}));
|
|
993
|
-
|
|
994
|
-
const postRequest = await findRequest(fetchImpl, 'POST', `/workflows/runs/${RUN_ID}/rerun`);
|
|
995
|
-
expect(postRequest).toBeDefined();
|
|
996
|
-
expect(postBodies).toEqual([{mode: 'failed'}]);
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
test('shows an error toast when rerun creation fails', async () => {
|
|
1000
|
-
const user = userEvent.setup();
|
|
1001
|
-
const errorSpy = vi.spyOn(toast, 'error').mockImplementation(() => 'toast-id');
|
|
1002
|
-
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
1003
|
-
const request = input as Request;
|
|
1004
|
-
const url = requestUrl(input);
|
|
1005
|
-
if (request.method === 'POST' && url.pathname === `/workflows/runs/${RUN_ID}/rerun`) {
|
|
1006
|
-
return Promise.resolve(
|
|
1007
|
-
jsonResponse({code: 'no-failed-jobs', message: 'Run has no failed jobs'}, {status: 409}),
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
return Promise.resolve(jsonResponse(workflowRunViewDetailDto({status: 'succeeded'})));
|
|
1011
|
-
});
|
|
1012
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
1013
|
-
|
|
1014
|
-
renderView();
|
|
1015
|
-
await user.click(await screen.findByRole('button', {name: 'Re-run workflow'}));
|
|
1016
|
-
|
|
1017
|
-
await waitFor(() => expect(errorSpy).toHaveBeenCalledWith('Run has no failed jobs'));
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
test('selects another run attempt and clears job selection search', async () => {
|
|
1021
|
-
const user = userEvent.setup();
|
|
1022
|
-
const secondAttemptId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
|
1023
|
-
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
1024
|
-
const url = requestUrl(input);
|
|
1025
|
-
if (url.pathname === `/workflows/runs/${RUN_ID}/attempts`) {
|
|
1026
|
-
return Promise.resolve(
|
|
1027
|
-
jsonResponse(
|
|
1028
|
-
runAttemptsResponseDto({
|
|
1029
|
-
attempts: [
|
|
1030
|
-
workflowRunAttemptDto({
|
|
1031
|
-
id: RUN_ID,
|
|
1032
|
-
attempt: 1,
|
|
1033
|
-
status: 'succeeded',
|
|
1034
|
-
created_at: '2026-05-07T01:01:00.000Z',
|
|
1035
|
-
}),
|
|
1036
|
-
workflowRunAttemptDto({
|
|
1037
|
-
id: secondAttemptId,
|
|
1038
|
-
attempt: 2,
|
|
1039
|
-
status: 'running',
|
|
1040
|
-
created_at: '2026-05-07T01:02:00.000Z',
|
|
1041
|
-
rerun_mode: 'all',
|
|
1042
|
-
}),
|
|
1043
|
-
],
|
|
1044
|
-
}),
|
|
1045
|
-
),
|
|
1046
|
-
);
|
|
1047
|
-
}
|
|
1048
|
-
return Promise.resolve(
|
|
1049
|
-
jsonResponse(
|
|
1050
|
-
workflowRunViewDetailDto({
|
|
1051
|
-
current_attempt: 1,
|
|
1052
|
-
latest_attempt: 2,
|
|
1053
|
-
run_attempt: workflowRunAttemptDto({id: RUN_ID, workflow_run_id: RUN_ID, attempt: 1}),
|
|
1054
|
-
}),
|
|
1055
|
-
),
|
|
1056
|
-
);
|
|
1057
|
-
});
|
|
1058
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
1059
|
-
|
|
1060
|
-
const {router} = renderProjectPage(
|
|
1061
|
-
`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}?job=${BUILD_JOB_ID}&step=${CHECKOUT_STEP_ID}&stepAttempt=${CHECKOUT_ATTEMPT_ID}`,
|
|
1062
|
-
() => (
|
|
1063
|
-
<WorkflowRunView
|
|
1064
|
-
projectId={PROJECT_ID}
|
|
1065
|
-
workspaceSlug={PROJECT_TEST_WSLUG}
|
|
1066
|
-
projectSlug="project"
|
|
1067
|
-
workflowRunId={RUN_ID}
|
|
1068
|
-
/>
|
|
1069
|
-
),
|
|
1070
|
-
);
|
|
1071
|
-
|
|
1072
|
-
await user.click(await screen.findByRole('button', {name: 'Switch attempt, currently 1 of 2'}));
|
|
1073
|
-
await user.click(await screen.findByRole('menuitem', {name: ATTEMPT_2_PATTERN}));
|
|
1074
|
-
|
|
1075
|
-
await waitFor(() =>
|
|
1076
|
-
expect(router.state.location.pathname).toBe(
|
|
1077
|
-
`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}`,
|
|
1078
|
-
),
|
|
1079
|
-
);
|
|
1080
|
-
expect(router.state.location.search).toEqual({runAttempt: 2});
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
test('does not render rerun controls for non-terminal runs', async () => {
|
|
144
|
+
test('shows the not-found surface when the run 404s', async () => {
|
|
1084
145
|
configureApiClient({
|
|
1085
|
-
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(
|
|
146
|
+
fetchImpl: vi.fn(() => Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}))),
|
|
1086
147
|
});
|
|
1087
148
|
|
|
1088
149
|
renderView();
|
|
1089
150
|
|
|
1090
|
-
await screen.
|
|
1091
|
-
expect(screen.queryByRole('button', {name: RERUN_BUTTON_NAME})).not.toBeInTheDocument();
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
test('renders carried-over steps without requesting attempt logs', async () => {
|
|
1095
|
-
const user = userEvent.setup();
|
|
1096
|
-
const fetchImpl = vi.fn(() =>
|
|
1097
|
-
Promise.resolve(
|
|
1098
|
-
jsonResponse(
|
|
1099
|
-
workflowRunViewDetailDto({
|
|
1100
|
-
status: 'succeeded',
|
|
1101
|
-
jobs: [
|
|
1102
|
-
workflowJobDto({
|
|
1103
|
-
id: BUILD_JOB_ID,
|
|
1104
|
-
run_attempt_id: RUN_ID,
|
|
1105
|
-
name: 'build',
|
|
1106
|
-
status: 'succeeded',
|
|
1107
|
-
carried_over: true,
|
|
1108
|
-
steps: [
|
|
1109
|
-
workflowStepDto({
|
|
1110
|
-
id: CHECKOUT_STEP_ID,
|
|
1111
|
-
key: 'checkout',
|
|
1112
|
-
name: 'checkout',
|
|
1113
|
-
status: 'succeeded',
|
|
1114
|
-
attempts: [],
|
|
1115
|
-
}),
|
|
1116
|
-
],
|
|
1117
|
-
}),
|
|
1118
|
-
],
|
|
1119
|
-
}),
|
|
1120
|
-
),
|
|
1121
|
-
),
|
|
1122
|
-
);
|
|
1123
|
-
configureApiClient({fetchImpl: fetchImpl as typeof fetch});
|
|
1124
|
-
|
|
1125
|
-
renderView();
|
|
1126
|
-
expect(
|
|
1127
|
-
await screen.findByRole('button', {name: 'build, Succeeded, reused'}),
|
|
1128
|
-
).toBeInTheDocument();
|
|
1129
|
-
expect(screen.getAllByText('reused')).toHaveLength(1);
|
|
1130
|
-
await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
|
|
1131
|
-
|
|
1132
|
-
expect(await screen.findByText('Not executed in this run.')).toBeInTheDocument();
|
|
1133
|
-
expect(
|
|
1134
|
-
mockRequests(fetchImpl).some((request) => requestUrl(request).pathname.includes('/logs')),
|
|
1135
|
-
).toBe(false);
|
|
151
|
+
expect(await screen.findByText('Run not found')).toBeInTheDocument();
|
|
1136
152
|
});
|
|
1137
153
|
});
|
|
1138
154
|
|
|
1139
|
-
function renderView(props: Partial<Parameters<typeof WorkflowRunView>[0]> = {}
|
|
1140
|
-
return renderProjectPage(`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}
|
|
155
|
+
function renderView(props: Partial<Parameters<typeof WorkflowRunView>[0]> = {}) {
|
|
156
|
+
return renderProjectPage(`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}`, () => (
|
|
1141
157
|
<WorkflowRunView
|
|
1142
158
|
projectId={PROJECT_ID}
|
|
1143
159
|
workspaceSlug={PROJECT_TEST_WSLUG}
|
|
1144
160
|
projectSlug="project"
|
|
1145
161
|
workflowRunId={RUN_ID}
|
|
1146
|
-
tab="jobs"
|
|
1147
162
|
{...props}
|
|
1148
163
|
/>
|
|
1149
164
|
));
|
|
1150
165
|
}
|
|
1151
166
|
|
|
1152
|
-
function requestUrl(input: RequestInfo | URL): URL {
|
|
1153
|
-
if (input instanceof Request) return new URL(input.url);
|
|
1154
|
-
return new URL(String(input));
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
async function findRequest(
|
|
1158
|
-
fetchImpl: ReturnType<typeof vi.fn>,
|
|
1159
|
-
method: string,
|
|
1160
|
-
pathname: string,
|
|
1161
|
-
): Promise<Request> {
|
|
1162
|
-
await waitFor(() => {
|
|
1163
|
-
const match = mockRequests(fetchImpl).find(
|
|
1164
|
-
(request) => request.method === method && requestUrl(request).pathname === pathname,
|
|
1165
|
-
);
|
|
1166
|
-
expect(match).toBeDefined();
|
|
1167
|
-
});
|
|
1168
|
-
const match = mockRequests(fetchImpl).find(
|
|
1169
|
-
(request) => request.method === method && requestUrl(request).pathname === pathname,
|
|
1170
|
-
);
|
|
1171
|
-
if (!match) throw new Error(`Missing ${method} ${pathname}`);
|
|
1172
|
-
return match;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
function mockRequests(fetchImpl: ReturnType<typeof vi.fn>): Request[] {
|
|
1176
|
-
return (fetchImpl.mock.calls as unknown[][])
|
|
1177
|
-
.map((call) => call[0])
|
|
1178
|
-
.filter((input): input is Request => input instanceof Request);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function locatedStepSourceDetail(
|
|
1182
|
-
overrides: Partial<WorkflowRunDetailResponseDto> = {},
|
|
1183
|
-
): WorkflowRunDetailResponseDto {
|
|
1184
|
-
return workflowRunViewDetailDto({
|
|
1185
|
-
source_snapshot: {
|
|
1186
|
-
format: 'yaml',
|
|
1187
|
-
content: 'jobs:\n build:\n steps:\n - run: pnpm test',
|
|
1188
|
-
},
|
|
1189
|
-
jobs: [
|
|
1190
|
-
workflowJobDto({
|
|
1191
|
-
id: BUILD_JOB_ID,
|
|
1192
|
-
name: 'build',
|
|
1193
|
-
status: 'succeeded',
|
|
1194
|
-
steps: [
|
|
1195
|
-
workflowStepDto({
|
|
1196
|
-
id: CHECKOUT_STEP_ID,
|
|
1197
|
-
name: 'checkout',
|
|
1198
|
-
status: 'succeeded',
|
|
1199
|
-
source_location: {start_line: 2, end_line: 3},
|
|
1200
|
-
attempts: [
|
|
1201
|
-
workflowStepAttemptDto({
|
|
1202
|
-
id: CHECKOUT_ATTEMPT_ID,
|
|
1203
|
-
step_id: CHECKOUT_STEP_ID,
|
|
1204
|
-
status: 'succeeded',
|
|
1205
|
-
exit_code: 0,
|
|
1206
|
-
finished_at: '2026-05-07T01:01:20.000Z',
|
|
1207
|
-
}),
|
|
1208
|
-
],
|
|
1209
|
-
}),
|
|
1210
|
-
],
|
|
1211
|
-
}),
|
|
1212
|
-
],
|
|
1213
|
-
...overrides,
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
167
|
function workflowRunViewDetailDto(
|
|
1218
168
|
overrides: Partial<WorkflowRunDetailResponseDto> = {},
|
|
1219
169
|
): WorkflowRunDetailResponseDto {
|
|
1220
170
|
return workflowRunDetailDto({
|
|
1221
171
|
id: RUN_ID,
|
|
1222
172
|
project_id: PROJECT_ID,
|
|
1223
|
-
definition_id: DEFINITION_ID,
|
|
1224
173
|
name: 'deploy-web',
|
|
1225
174
|
status: 'running',
|
|
1226
175
|
trigger_payload: {},
|
|
@@ -1232,23 +181,7 @@ function workflowRunViewDetailDto(
|
|
|
1232
181
|
run_attempt_id: RUN_ID,
|
|
1233
182
|
name: 'build',
|
|
1234
183
|
status: 'succeeded',
|
|
1235
|
-
steps: [
|
|
1236
|
-
workflowStepDto({
|
|
1237
|
-
id: CHECKOUT_STEP_ID,
|
|
1238
|
-
key: 'checkout',
|
|
1239
|
-
name: 'checkout',
|
|
1240
|
-
status: 'succeeded',
|
|
1241
|
-
attempts: [
|
|
1242
|
-
workflowStepAttemptDto({
|
|
1243
|
-
id: CHECKOUT_ATTEMPT_ID,
|
|
1244
|
-
step_id: CHECKOUT_STEP_ID,
|
|
1245
|
-
status: 'succeeded',
|
|
1246
|
-
exit_code: 0,
|
|
1247
|
-
finished_at: '2026-05-07T01:01:20.000Z',
|
|
1248
|
-
}),
|
|
1249
|
-
],
|
|
1250
|
-
}),
|
|
1251
|
-
],
|
|
184
|
+
steps: [workflowStepDto({name: 'checkout', status: 'succeeded'})],
|
|
1252
185
|
}),
|
|
1253
186
|
workflowJobDto({
|
|
1254
187
|
id: DEPLOY_JOB_ID,
|
|
@@ -1257,14 +190,7 @@ function workflowRunViewDetailDto(
|
|
|
1257
190
|
status: 'running',
|
|
1258
191
|
position: 1,
|
|
1259
192
|
dependencies: ['build'],
|
|
1260
|
-
|
|
1261
|
-
workflowJobExecutionDto({
|
|
1262
|
-
job_id: DEPLOY_JOB_ID,
|
|
1263
|
-
name: 'deploy',
|
|
1264
|
-
status: 'running',
|
|
1265
|
-
steps: [workflowStepDto({status: 'running'})],
|
|
1266
|
-
}),
|
|
1267
|
-
],
|
|
193
|
+
steps: [workflowStepDto({name: 'deploy', status: 'running'})],
|
|
1268
194
|
}),
|
|
1269
195
|
],
|
|
1270
196
|
...overrides,
|