@shipfox/client-workflows 13.0.0 → 14.0.1

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.
Files changed (133) hide show
  1. package/.storybook/main.ts +16 -1
  2. package/.storybook/workspace-source.ts +31 -0
  3. package/.turbo/turbo-build.log +1 -1
  4. package/CHANGELOG.md +37 -0
  5. package/dist/components/job-graph/job-duration-label.js +1 -1
  6. package/dist/components/job-graph/job-duration-label.js.map +1 -1
  7. package/dist/components/job-graph/job-graph-content.d.ts +2 -1
  8. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  9. package/dist/components/job-graph/job-graph-content.js +3 -3
  10. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  11. package/dist/components/job-graph/job-graph-view.d.ts +2 -1
  12. package/dist/components/job-graph/job-graph-view.d.ts.map +1 -1
  13. package/dist/components/job-graph/job-graph-view.js +2 -2
  14. package/dist/components/job-graph/job-graph-view.js.map +1 -1
  15. package/dist/components/job-graph/job-node.js +5 -5
  16. package/dist/components/job-graph/job-node.js.map +1 -1
  17. package/dist/components/job-graph/types.d.ts +2 -1
  18. package/dist/components/job-graph/types.d.ts.map +1 -1
  19. package/dist/components/job-graph/types.js.map +1 -1
  20. package/dist/components/workflow-run-duration-label.js +1 -1
  21. package/dist/components/workflow-run-duration-label.js.map +1 -1
  22. package/dist/components/workflow-run-number-label.js +1 -1
  23. package/dist/components/workflow-run-number-label.js.map +1 -1
  24. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +2 -2
  25. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  26. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +2 -7
  27. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  28. package/dist/components/workflow-run-summary/workflow-run-summary.js +92 -68
  29. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  30. package/dist/components/workflow-run-tabs/index.d.ts +6 -0
  31. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -0
  32. package/dist/components/workflow-run-tabs/index.js +7 -0
  33. package/dist/components/workflow-run-tabs/index.js.map +1 -0
  34. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +11 -0
  35. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -0
  36. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +71 -0
  37. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -0
  38. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +2 -0
  39. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -0
  40. package/dist/components/workflow-run-tabs/run-annotations-empty.js +11 -0
  41. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -0
  42. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +18 -0
  43. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +1 -0
  44. package/dist/components/workflow-run-tabs/run-jobs-list.js +107 -0
  45. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +1 -0
  46. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +6 -0
  47. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +1 -0
  48. package/dist/components/workflow-run-tabs/run-tab-count.js +27 -0
  49. package/dist/components/workflow-run-tabs/run-tab-count.js.map +1 -0
  50. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +8 -0
  51. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +1 -0
  52. package/dist/components/workflow-run-tabs/run-tab-strip.js +82 -0
  53. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +1 -0
  54. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  55. package/dist/components/workflow-run-view/workflow-run-selection.js +40 -1
  56. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  57. package/dist/components/workflow-run-view/workflow-run-states.d.ts +1 -0
  58. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
  59. package/dist/components/workflow-run-view/workflow-run-states.js +10 -1
  60. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  61. package/dist/components/workflow-run-view/workflow-run-view.d.ts +8 -4
  62. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  63. package/dist/components/workflow-run-view/workflow-run-view.js +125 -78
  64. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  65. package/dist/components/workflow-source-panel/index.d.ts +1 -0
  66. package/dist/components/workflow-source-panel/index.d.ts.map +1 -1
  67. package/dist/components/workflow-source-panel/index.js +1 -0
  68. package/dist/components/workflow-source-panel/index.js.map +1 -1
  69. package/dist/components/workflow-source-panel/workflow-source-content.d.ts +12 -0
  70. package/dist/components/workflow-source-panel/workflow-source-content.d.ts.map +1 -0
  71. package/dist/components/workflow-source-panel/workflow-source-content.js +53 -0
  72. package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -0
  73. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +1 -1
  74. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -1
  75. package/dist/components/workflow-source-panel/workflow-source-panel.js +7 -54
  76. package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -1
  77. package/dist/core/entities/step.d.ts +1 -1
  78. package/dist/core/entities/step.d.ts.map +1 -1
  79. package/dist/core/entities/step.js +2 -0
  80. package/dist/core/entities/step.js.map +1 -1
  81. package/dist/core/workflow-run-tabs.d.ts +9 -0
  82. package/dist/core/workflow-run-tabs.d.ts.map +1 -0
  83. package/dist/core/workflow-run-tabs.js +3 -0
  84. package/dist/core/workflow-run-tabs.js.map +1 -0
  85. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
  86. package/dist/pages/workflow-run-detail-page.js +28 -6
  87. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  88. package/dist/routes/inputs.d.ts +33 -0
  89. package/dist/routes/inputs.d.ts.map +1 -1
  90. package/dist/routes/inputs.js +49 -0
  91. package/dist/routes/inputs.js.map +1 -1
  92. package/dist/tsconfig.test.tsbuildinfo +1 -1
  93. package/package.json +9 -9
  94. package/src/components/job-graph/job-duration-label.tsx +5 -1
  95. package/src/components/job-graph/job-graph-content.tsx +7 -4
  96. package/src/components/job-graph/job-graph-view.tsx +6 -3
  97. package/src/components/job-graph/job-graph.stories.tsx +1 -1
  98. package/src/components/job-graph/job-node.stories.tsx +1 -1
  99. package/src/components/job-graph/job-node.test.tsx +27 -2
  100. package/src/components/job-graph/job-node.tsx +6 -6
  101. package/src/components/job-graph/types.ts +5 -1
  102. package/src/components/workflow-run-duration-label.tsx +1 -1
  103. package/src/components/workflow-run-number-label.tsx +1 -1
  104. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +2 -2
  105. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +23 -30
  106. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +3 -22
  107. package/src/components/workflow-run-summary/workflow-run-summary.tsx +84 -69
  108. package/src/components/workflow-run-tabs/index.ts +5 -0
  109. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +69 -0
  110. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +97 -0
  111. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +11 -0
  112. package/src/components/workflow-run-tabs/run-jobs-list.tsx +151 -0
  113. package/src/components/workflow-run-tabs/run-tab-count.tsx +28 -0
  114. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +52 -0
  115. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +117 -0
  116. package/src/components/workflow-run-tabs/run-tab-strip.tsx +97 -0
  117. package/src/components/workflow-run-view/workflow-run-selection.test.ts +48 -0
  118. package/src/components/workflow-run-view/workflow-run-selection.ts +48 -1
  119. package/src/components/workflow-run-view/workflow-run-states.tsx +12 -1
  120. package/src/components/workflow-run-view/workflow-run-view.test.tsx +64 -64
  121. package/src/components/workflow-run-view/workflow-run-view.tsx +199 -85
  122. package/src/components/workflow-source-panel/index.ts +1 -0
  123. package/src/components/workflow-source-panel/workflow-source-content.tsx +84 -0
  124. package/src/components/workflow-source-panel/workflow-source-panel.tsx +14 -81
  125. package/src/core/entities/step.ts +4 -0
  126. package/src/core/workflow-run-tabs.ts +8 -0
  127. package/src/pages/workflow-run-detail-page.tsx +31 -4
  128. package/src/pages/workflow-run-pages.test.tsx +185 -45
  129. package/src/routes/inputs.test.ts +47 -0
  130. package/src/routes/inputs.ts +45 -0
  131. package/src/storybook-config.test.ts +27 -0
  132. package/tsconfig.build.tsbuildinfo +1 -1
  133. package/tsconfig.test.json +7 -1
@@ -82,6 +82,29 @@ describe('resolveWorkflowRunSelection', () => {
82
82
  expect(resolved.selectedAttemptId).toBeNull();
83
83
  });
84
84
 
85
+ test('finds a job from a legacy job execution selection', () => {
86
+ const execution = workflowJobExecutionDto({id: 'execution-deploy', sequence: 1});
87
+ const run = workflowRunDetail({
88
+ jobs: [
89
+ workflowJobDto({id: 'job-build', name: 'build'}),
90
+ workflowJobDto({
91
+ id: 'job-deploy',
92
+ name: 'deploy',
93
+ position: 1,
94
+ job_executions: [execution],
95
+ }),
96
+ ],
97
+ });
98
+
99
+ const resolved = resolveWorkflowRunSelection({
100
+ run,
101
+ selection: {jobExecutionId: 'execution-deploy'},
102
+ });
103
+
104
+ expect(resolved.job?.id).toBe('job-deploy');
105
+ expect(resolved.jobExecution?.id).toBe('execution-deploy');
106
+ });
107
+
85
108
  test('falls back from an invalid job execution id to the running execution', () => {
86
109
  const run = workflowRunDetail({
87
110
  jobs: [
@@ -156,6 +179,31 @@ describe('resolveWorkflowRunSelection', () => {
156
179
  expect(resolved.selectedAttemptId).toBe('attempt-2');
157
180
  });
158
181
 
182
+ test('finds the step owner from a legacy step attempt selection', () => {
183
+ const attempt = workflowStepAttemptDto({id: 'attempt-deploy', step_id: 'step-deploy'});
184
+ const step = workflowStepDto({
185
+ id: 'step-deploy',
186
+ current_attempt: 1,
187
+ attempts: [attempt],
188
+ });
189
+ const run = workflowRunDetail({
190
+ jobs: [
191
+ workflowJobDto({id: 'job-build', name: 'build'}),
192
+ workflowJobDto({id: 'job-deploy', name: 'deploy', position: 1, steps: [step]}),
193
+ ],
194
+ });
195
+
196
+ const resolved = resolveWorkflowRunSelection({
197
+ run,
198
+ selection: {stepAttemptId: 'attempt-deploy'},
199
+ });
200
+
201
+ expect(resolved.job?.id).toBe('job-deploy');
202
+ expect(resolved.step?.id).toBe('step-deploy');
203
+ expect(resolved.attempt?.id).toBe('attempt-deploy');
204
+ expect(resolved.selectedAttemptId).toBe('attempt-deploy');
205
+ });
206
+
159
207
  test('lets a valid step override a mismatched job id', () => {
160
208
  const step = workflowStepDto({
161
209
  id: 'step-deploy',
@@ -25,6 +25,8 @@ export function resolveWorkflowRunSelection({
25
25
  }): ResolvedWorkflowRunSelection {
26
26
  const jobById = new Map(run.jobs.map((job) => [job.id, job]));
27
27
  const stepMatch = findStep(run, selection.stepId);
28
+ const jobExecutionMatch = findJobExecution(run, selection.jobExecutionId);
29
+ const stepAttemptMatch = findStepAttempt(run, selection.stepAttemptId);
28
30
 
29
31
  if (stepMatch) {
30
32
  const attempt = resolveStepAttempt(stepMatch.step, selection.stepAttemptId);
@@ -37,7 +39,20 @@ export function resolveWorkflowRunSelection({
37
39
  };
38
40
  }
39
41
 
40
- const job = (selection.jobId ? jobById.get(selection.jobId) : undefined) ?? run.jobs.at(0);
42
+ if (stepAttemptMatch) {
43
+ return {
44
+ job: stepAttemptMatch.job,
45
+ jobExecution: stepAttemptMatch.jobExecution,
46
+ step: stepAttemptMatch.step,
47
+ attempt: stepAttemptMatch.attempt,
48
+ selectedAttemptId: stepAttemptMatch.attempt.id,
49
+ };
50
+ }
51
+
52
+ const job =
53
+ (selection.jobId ? jobById.get(selection.jobId) : undefined) ??
54
+ jobExecutionMatch?.job ??
55
+ run.jobs.at(0);
41
56
  const jobExecution = job ? resolveJobExecution(job, selection.jobExecutionId) : undefined;
42
57
 
43
58
  return {
@@ -49,6 +64,38 @@ export function resolveWorkflowRunSelection({
49
64
  };
50
65
  }
51
66
 
67
+ function findJobExecution(
68
+ run: WorkflowRunDetail,
69
+ jobExecutionId: string | undefined,
70
+ ): {job: Job; jobExecution: JobExecution} | undefined {
71
+ if (!jobExecutionId) return undefined;
72
+
73
+ for (const job of run.jobs) {
74
+ const jobExecution = job.jobExecutions.find((candidate) => candidate.id === jobExecutionId);
75
+ if (jobExecution) return {job, jobExecution};
76
+ }
77
+
78
+ return undefined;
79
+ }
80
+
81
+ function findStepAttempt(
82
+ run: WorkflowRunDetail,
83
+ stepAttemptId: string | undefined,
84
+ ): {job: Job; jobExecution: JobExecution; step: Step; attempt: StepAttempt} | undefined {
85
+ if (!stepAttemptId) return undefined;
86
+
87
+ for (const job of run.jobs) {
88
+ for (const jobExecution of job.jobExecutions) {
89
+ for (const step of jobExecution.steps) {
90
+ const attempt = step.attempts.find((candidate) => candidate.id === stepAttemptId);
91
+ if (attempt) return {job, jobExecution, step, attempt};
92
+ }
93
+ }
94
+ }
95
+
96
+ return undefined;
97
+ }
98
+
52
99
  function findStep(
53
100
  run: WorkflowRunDetail,
54
101
  stepId: string | undefined,
@@ -9,7 +9,7 @@ export function WorkflowRunSkeleton() {
9
9
  return (
10
10
  <section
11
11
  aria-label="Loading workflow run"
12
- className="border-b border-border-neutral-base bg-background-subtle-base px-16 py-12"
12
+ className="bg-background-neutral-background px-16 py-12"
13
13
  >
14
14
  <div className="flex min-w-0 flex-wrap items-center gap-x-12 gap-y-8">
15
15
  <div className="flex min-w-0 items-center gap-8">
@@ -31,6 +31,17 @@ export function WorkflowRunSkeleton() {
31
31
  );
32
32
  }
33
33
 
34
+ export function WorkflowRunContentSkeleton() {
35
+ return (
36
+ <section
37
+ aria-label="Loading workflow run content"
38
+ className="min-h-160 rounded-8 border border-border-neutral-base bg-background-components-base p-16"
39
+ >
40
+ <Skeleton className="h-160 w-full rounded-6" />
41
+ </section>
42
+ );
43
+ }
44
+
34
45
  export function WorkflowRunNotFound() {
35
46
  return (
36
47
  <EmptyState
@@ -26,6 +26,8 @@ const DEPLOY_JOB_ID = '88888888-8888-4888-8888-888888888888';
26
26
  const CHECKOUT_STEP_ID = '99999999-9999-4999-8999-999999999999';
27
27
  const CHECKOUT_ATTEMPT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
28
28
  const RERUN_BUTTON_NAME = /^Re-run/;
29
+ const BUILD_JOB_BUTTON_NAME = 'build, Succeeded';
30
+ const DEPLOY_JOB_BUTTON_NAME = 'deploy, Running';
29
31
  const ATTEMPT_2_PATTERN = /Attempt 2/;
30
32
  const COPY_RUN_BUTTON_NAME = /Copy run/;
31
33
  const EXECUTION_ONE_MENU_ITEM_PATTERN = /#1/;
@@ -34,7 +36,19 @@ const LISTENING_EXECUTION_SWITCHER_NAME =
34
36
  const JOB_TEMPLATE_NAME = ['Implement $', '{{ event.issue.identifier }}'].join('');
35
37
 
36
38
  describe('WorkflowRunView', () => {
37
- test('renders the run summary, jobs graph, and selected job step attempts when a run loads', async () => {
39
+ test('keeps the real tab strip visible while the run is loading', async () => {
40
+ configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
41
+
42
+ renderView({tab: 'summary'});
43
+
44
+ expect(await screen.findByRole('tab', {name: 'Summary'})).toBeInTheDocument();
45
+ expect(screen.getByRole('tab', {name: 'Jobs'})).toBeInTheDocument();
46
+ expect(screen.getByRole('tab', {name: 'Annotations'})).toBeInTheDocument();
47
+ expect(screen.getByRole('tab', {name: 'Source'})).toBeInTheDocument();
48
+ expect(screen.getByRole('region', {name: 'Loading workflow run content'})).toBeInTheDocument();
49
+ });
50
+
51
+ test('renders the run summary, jobs list, and selected job step attempts when a run loads', async () => {
38
52
  configureApiClient({
39
53
  fetchImpl: vi.fn(() => Promise.resolve(jsonResponse(workflowRunViewDetailDto()))),
40
54
  });
@@ -50,8 +64,13 @@ describe('WorkflowRunView', () => {
50
64
  within(summary).queryByRole('button', {name: COPY_RUN_BUTTON_NAME}),
51
65
  ).not.toBeInTheDocument();
52
66
  expect(await screen.findByRole('region', {name: 'Workflow jobs'})).toBeInTheDocument();
53
- expect(screen.getByRole('button', {name: 'build, Succeeded'})).toBeInTheDocument();
54
- expect(screen.getByRole('button', {name: 'deploy, Running'})).toBeInTheDocument();
67
+ expect(screen.getByRole('button', {name: BUILD_JOB_BUTTON_NAME})).toBeInTheDocument();
68
+ expect(screen.getByRole('button', {name: DEPLOY_JOB_BUTTON_NAME})).toBeInTheDocument();
69
+ expect(screen.getByRole('button', {name: BUILD_JOB_BUTTON_NAME})).toHaveClass(
70
+ 'bg-background-components-hover',
71
+ 'hover:bg-background-components-pressed',
72
+ );
73
+ expect(screen.getByRole('tabpanel')).toHaveClass('max-w-[1120px]', 'px-24');
55
74
  expect(screen.getByRole('region', {name: 'build'})).toBeInTheDocument();
56
75
  expect(
57
76
  screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}),
@@ -685,8 +704,7 @@ describe('WorkflowRunView', () => {
685
704
  ).toBeInTheDocument();
686
705
  });
687
706
 
688
- test('opens and closes workflow source without resetting the selected job', async () => {
689
- const user = userEvent.setup();
707
+ test('renders the full workflow source in the Source tab', async () => {
690
708
  configureApiClient({
691
709
  fetchImpl: vi.fn(() =>
692
710
  Promise.resolve(
@@ -702,38 +720,20 @@ describe('WorkflowRunView', () => {
702
720
  ),
703
721
  });
704
722
 
705
- renderView();
706
-
707
- const deployNode = await screen.findByRole('button', {name: 'deploy, Running'});
708
- await user.click(deployNode);
709
- expect(deployNode).toHaveAttribute('aria-pressed', 'true');
723
+ renderView({tab: 'source'});
710
724
 
711
- const sourceButton = within(screen.getByRole('region', {name: 'deploy-web'})).getByRole(
712
- 'button',
713
- {name: 'View source'},
725
+ expect(await screen.findByRole('tab', {name: 'Source'})).toHaveAttribute(
726
+ 'aria-selected',
727
+ 'true',
714
728
  );
715
- const panelId = sourceButton.getAttribute('aria-controls');
716
- expect(panelId).toBeTruthy();
717
- expect(sourceButton).toHaveAttribute('aria-expanded', 'false');
718
-
719
- await user.click(sourceButton);
720
-
721
- const sourcePanel = screen.getByRole('dialog', {name: 'Workflow source'});
722
- expect(sourceButton).toHaveAttribute('aria-expanded', 'true');
723
- expect(sourcePanel).toHaveAttribute('id', panelId);
724
- expect(sourcePanel).toHaveTextContent('pnpm test');
725
-
726
- await user.click(screen.getByRole('button', {name: 'Close source'}));
727
-
728
- await waitFor(() => expect(sourceButton).toHaveFocus());
729
- expect(sourceButton).toHaveAttribute('aria-expanded', 'false');
730
- expect(deployNode).toHaveAttribute('aria-pressed', 'true');
731
- expect(screen.getByRole('region', {name: 'deploy'})).toBeInTheDocument();
729
+ expect(await screen.findByText('pnpm test')).toBeInTheDocument();
730
+ expect(screen.queryByRole('dialog', {name: 'Workflow source'})).not.toBeInTheDocument();
732
731
  });
733
732
 
734
733
  test('highlights selected step source lines when the source panel opens', async () => {
735
734
  const user = userEvent.setup();
736
735
  const stepId = '99999999-9999-4999-8999-000000000003';
736
+ const stepAttemptId = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000003';
737
737
  configureApiClient({
738
738
  fetchImpl: vi.fn(() =>
739
739
  Promise.resolve(
@@ -749,13 +749,27 @@ describe('WorkflowRunView', () => {
749
749
  run_attempt_id: RUN_ID,
750
750
  name: 'deploy',
751
751
  status: 'running',
752
- steps: [
753
- workflowStepDto({
754
- id: stepId,
755
- key: 'deploy',
752
+ job_executions: [
753
+ workflowJobExecutionDto({
754
+ job_id: DEPLOY_JOB_ID,
756
755
  name: 'deploy',
757
- source_location: {start_line: 2, end_line: 3},
758
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
+ ],
759
773
  }),
760
774
  ],
761
775
  }),
@@ -766,29 +780,29 @@ describe('WorkflowRunView', () => {
766
780
  ),
767
781
  });
768
782
 
769
- renderView({selection: {stepId}});
770
- const summary = await screen.findByRole('region', {name: 'deploy-web'});
771
- await user.click(within(summary).getByRole('button', {name: 'View source'}));
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'}));
772
786
 
773
- await screen.findByRole('dialog', {name: 'Workflow source'});
774
- const highlightedLines = document.body.querySelectorAll('.line.highlighted-line');
787
+ const sourceDialog = await screen.findByRole('dialog', {name: 'Workflow source'});
788
+ const highlightedLines = sourceDialog.querySelectorAll('.line.highlighted-line');
775
789
  expect(highlightedLines).toHaveLength(2);
776
790
  expect(highlightedLines[0]).toHaveTextContent('deploy:');
777
791
  expect(highlightedLines[1]).toHaveTextContent('steps:');
778
792
  });
779
793
 
780
- test('does not render a source control when the run has no source snapshot', async () => {
794
+ test('explains when the run has no source snapshot', async () => {
781
795
  configureApiClient({
782
796
  fetchImpl: vi.fn(() =>
783
797
  Promise.resolve(jsonResponse(workflowRunViewDetailDto({source_snapshot: null}))),
784
798
  ),
785
799
  });
786
800
 
787
- renderView();
801
+ renderView({tab: 'source'});
788
802
 
789
803
  await screen.findByRole('region', {name: 'deploy-web'});
790
804
 
791
- expect(screen.queryByRole('button', {name: 'View source'})).not.toBeInTheDocument();
805
+ expect(await screen.findByText('Source snapshot unavailable')).toBeInTheDocument();
792
806
  });
793
807
 
794
808
  test('opens the source panel highlighting a step from the job header Source button', async () => {
@@ -802,25 +816,17 @@ describe('WorkflowRunView', () => {
802
816
  const jobRegion = await screen.findByRole('region', {name: 'build'});
803
817
  await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
804
818
  const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
805
- const summaryButton = within(screen.getByRole('region', {name: 'deploy-web'})).getByRole(
806
- 'button',
807
- {name: 'View source'},
808
- );
809
819
  expect(jobSourceButton).toHaveAttribute('aria-expanded', 'false');
810
- expect(jobSourceButton).toHaveAttribute(
811
- 'aria-controls',
812
- summaryButton.getAttribute('aria-controls'),
813
- );
820
+ expect(jobSourceButton).toHaveAttribute('aria-controls');
814
821
 
815
822
  await user.click(jobSourceButton);
816
823
 
817
- await screen.findByRole('dialog', {name: 'Workflow source'});
818
- const highlighted = document.body.querySelectorAll('.line.highlighted-line');
824
+ const sourceDialog = await screen.findByRole('dialog', {name: 'Workflow source'});
825
+ const highlighted = sourceDialog.querySelectorAll('.line.highlighted-line');
819
826
  expect(highlighted).toHaveLength(2);
820
827
  expect(highlighted[0]).toHaveTextContent('build:');
821
828
  expect(highlighted[1]).toHaveTextContent('steps:');
822
829
  expect(jobSourceButton).toHaveAttribute('aria-expanded', 'true');
823
- expect(summaryButton).toHaveAttribute('aria-expanded', 'false');
824
830
  });
825
831
 
826
832
  test('returns focus to the job Source button on close and preserves the selected job', async () => {
@@ -844,7 +850,7 @@ describe('WorkflowRunView', () => {
844
850
  expect(screen.getByRole('region', {name: 'build'})).toBeInTheDocument();
845
851
  });
846
852
 
847
- test('falls back to the summary Source button when refetch removes the focused step source', async () => {
853
+ test('closes the source panel when refetch removes the focused step source', async () => {
848
854
  const user = userEvent.setup();
849
855
  let detailRequests = 0;
850
856
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
@@ -893,10 +899,6 @@ describe('WorkflowRunView', () => {
893
899
  const jobRegion = await screen.findByRole('region', {name: 'build'});
894
900
  await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
895
901
  const jobSourceButton = within(jobRegion).getByRole('button', {name: 'View source'});
896
- const summaryButton = within(screen.getByRole('region', {name: 'deploy-web'})).getByRole(
897
- 'button',
898
- {name: 'View source'},
899
- );
900
902
  await user.click(jobSourceButton);
901
903
  await screen.findByRole('dialog', {name: 'Workflow source'});
902
904
 
@@ -906,10 +908,7 @@ describe('WorkflowRunView', () => {
906
908
  within(jobRegion).queryByRole('button', {name: 'View source'}),
907
909
  ).not.toBeInTheDocument(),
908
910
  );
909
- await user.click(screen.getByRole('button', {name: 'Close source'}));
910
-
911
- await waitFor(() => expect(summaryButton).toHaveFocus());
912
- expect(summaryButton).toHaveAttribute('aria-expanded', 'false');
911
+ expect(screen.queryByRole('dialog', {name: 'Workflow source'})).not.toBeInTheDocument();
913
912
  });
914
913
 
915
914
  test('re-runs all jobs from a succeeded run and navigates to the new run', async () => {
@@ -1127,7 +1126,7 @@ describe('WorkflowRunView', () => {
1127
1126
  expect(
1128
1127
  await screen.findByRole('button', {name: 'build, Succeeded, reused'}),
1129
1128
  ).toBeInTheDocument();
1130
- expect(screen.getAllByText('reused')).toHaveLength(2);
1129
+ expect(screen.getAllByText('reused')).toHaveLength(1);
1131
1130
  await user.click(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'}));
1132
1131
 
1133
1132
  expect(await screen.findByText('Not executed in this run.')).toBeInTheDocument();
@@ -1144,6 +1143,7 @@ function renderView(props: Partial<Parameters<typeof WorkflowRunView>[0]> = {},
1144
1143
  workspaceSlug={PROJECT_TEST_WSLUG}
1145
1144
  projectSlug="project"
1146
1145
  workflowRunId={RUN_ID}
1146
+ tab="jobs"
1147
1147
  {...props}
1148
1148
  />
1149
1149
  ));