@shipfox/client-workflows 12.0.2 → 14.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.
Files changed (325) hide show
  1. package/.storybook/main.ts +16 -1
  2. package/.storybook/preview.tsx +3 -3
  3. package/.storybook/workspace-source.ts +31 -0
  4. package/.turbo/turbo-build.log +1 -1
  5. package/CHANGELOG.md +79 -0
  6. package/dist/components/job-graph/job-duration-format.d.ts +2 -2
  7. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -1
  8. package/dist/components/job-graph/job-duration-format.js +4 -2
  9. package/dist/components/job-graph/job-duration-format.js.map +1 -1
  10. package/dist/components/job-graph/job-duration-label.js +1 -1
  11. package/dist/components/job-graph/job-duration-label.js.map +1 -1
  12. package/dist/components/job-graph/job-graph-content.d.ts +2 -1
  13. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  14. package/dist/components/job-graph/job-graph-content.js +3 -3
  15. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  16. package/dist/components/job-graph/job-graph-view.d.ts +2 -1
  17. package/dist/components/job-graph/job-graph-view.d.ts.map +1 -1
  18. package/dist/components/job-graph/job-graph-view.js +2 -2
  19. package/dist/components/job-graph/job-graph-view.js.map +1 -1
  20. package/dist/components/job-graph/job-node.d.ts +1 -1
  21. package/dist/components/job-graph/job-node.d.ts.map +1 -1
  22. package/dist/components/job-graph/job-node.js +10 -9
  23. package/dist/components/job-graph/job-node.js.map +1 -1
  24. package/dist/components/job-graph/types.d.ts +2 -1
  25. package/dist/components/job-graph/types.d.ts.map +1 -1
  26. package/dist/components/job-graph/types.js.map +1 -1
  27. package/dist/components/step-list/step-list.d.ts +2 -2
  28. package/dist/components/step-list/step-list.d.ts.map +1 -1
  29. package/dist/components/step-list/step-list.js.map +1 -1
  30. package/dist/components/workflow-run-duration-label.js +1 -1
  31. package/dist/components/workflow-run-duration-label.js.map +1 -1
  32. package/dist/components/workflow-run-list/job-status-strip.d.ts +21 -0
  33. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -0
  34. package/dist/components/workflow-run-list/job-status-strip.js +185 -0
  35. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -0
  36. package/dist/components/workflow-run-list/run-display.d.ts +27 -3
  37. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -1
  38. package/dist/components/workflow-run-list/run-display.js +78 -7
  39. package/dist/components/workflow-run-list/run-display.js.map +1 -1
  40. package/dist/components/workflow-run-list/types.d.ts +23 -24
  41. package/dist/components/workflow-run-list/types.d.ts.map +1 -1
  42. package/dist/components/workflow-run-list/types.js.map +1 -1
  43. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts +22 -0
  44. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts.map +1 -0
  45. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +68 -0
  46. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -0
  47. package/dist/components/workflow-run-list/workflow-run-filters.d.ts +18 -0
  48. package/dist/components/workflow-run-list/workflow-run-filters.d.ts.map +1 -0
  49. package/dist/components/workflow-run-list/workflow-run-filters.js +236 -0
  50. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -0
  51. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +8 -4
  52. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  53. package/dist/components/workflow-run-list/workflow-run-list-content.js +40 -12
  54. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  55. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +29 -2
  56. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -44
  58. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  59. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
  60. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  61. package/dist/components/workflow-run-list/workflow-run-list-view.js +60 -43
  62. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
  65. package/dist/components/workflow-run-list/workflow-run-list.js +20 -8
  66. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
  67. package/dist/components/workflow-run-list/workflow-run-row.d.ts +19 -9
  68. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  69. package/dist/components/workflow-run-list/workflow-run-row.js +179 -112
  70. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  71. package/dist/components/workflow-run-number-label.d.ts +15 -0
  72. package/dist/components/workflow-run-number-label.d.ts.map +1 -0
  73. package/dist/components/workflow-run-number-label.js +41 -0
  74. package/dist/components/workflow-run-number-label.js.map +1 -0
  75. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +3 -3
  76. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -1
  77. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +10 -9
  78. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  79. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +4 -9
  80. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  81. package/dist/components/workflow-run-summary/workflow-run-summary.js +107 -74
  82. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  83. package/dist/components/workflow-run-tabs/index.d.ts +6 -0
  84. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -0
  85. package/dist/components/workflow-run-tabs/index.js +7 -0
  86. package/dist/components/workflow-run-tabs/index.js.map +1 -0
  87. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +11 -0
  88. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -0
  89. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +71 -0
  90. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -0
  91. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +2 -0
  92. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -0
  93. package/dist/components/workflow-run-tabs/run-annotations-empty.js +11 -0
  94. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -0
  95. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +18 -0
  96. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +1 -0
  97. package/dist/components/workflow-run-tabs/run-jobs-list.js +107 -0
  98. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +1 -0
  99. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +6 -0
  100. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +1 -0
  101. package/dist/components/workflow-run-tabs/run-tab-count.js +27 -0
  102. package/dist/components/workflow-run-tabs/run-tab-count.js.map +1 -0
  103. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +8 -0
  104. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +1 -0
  105. package/dist/components/workflow-run-tabs/run-tab-strip.js +82 -0
  106. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +1 -0
  107. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +2 -2
  108. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -1
  109. package/dist/components/workflow-run-view/agent-config-failure-callout.js +4 -4
  110. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -1
  111. package/dist/components/workflow-run-view/job-card.d.ts +2 -2
  112. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
  113. package/dist/components/workflow-run-view/job-card.js +51 -32
  114. package/dist/components/workflow-run-view/job-card.js.map +1 -1
  115. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +1 -1
  116. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -1
  117. package/dist/components/workflow-run-view/job-execution-switcher.js +20 -8
  118. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -1
  119. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  120. package/dist/components/workflow-run-view/workflow-run-selection.js +40 -1
  121. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  122. package/dist/components/workflow-run-view/workflow-run-states.d.ts +1 -0
  123. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
  124. package/dist/components/workflow-run-view/workflow-run-states.js +10 -1
  125. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  126. package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -5
  127. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  128. package/dist/components/workflow-run-view/workflow-run-view.js +136 -85
  129. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  130. package/dist/components/workflow-source-panel/index.d.ts +1 -0
  131. package/dist/components/workflow-source-panel/index.d.ts.map +1 -1
  132. package/dist/components/workflow-source-panel/index.js +1 -0
  133. package/dist/components/workflow-source-panel/index.js.map +1 -1
  134. package/dist/components/workflow-source-panel/workflow-source-content.d.ts +12 -0
  135. package/dist/components/workflow-source-panel/workflow-source-content.d.ts.map +1 -0
  136. package/dist/components/workflow-source-panel/workflow-source-content.js +53 -0
  137. package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -0
  138. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +1 -1
  139. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -1
  140. package/dist/components/workflow-source-panel/workflow-source-panel.js +7 -54
  141. package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -1
  142. package/dist/components/workflow-status/status-visuals.d.ts +4 -4
  143. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  144. package/dist/components/workflow-status/status-visuals.js +7 -0
  145. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  146. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -4
  147. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  148. package/dist/components/workflow-status/workflow-status-icon.js +6 -4
  149. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  150. package/dist/core/entities/job-execution.d.ts +10 -0
  151. package/dist/core/entities/job-execution.d.ts.map +1 -1
  152. package/dist/core/entities/job-execution.js +11 -0
  153. package/dist/core/entities/job-execution.js.map +1 -1
  154. package/dist/core/entities/job.d.ts +4 -2
  155. package/dist/core/entities/job.d.ts.map +1 -1
  156. package/dist/core/entities/job.js +8 -1
  157. package/dist/core/entities/job.js.map +1 -1
  158. package/dist/core/entities/step.d.ts +1 -1
  159. package/dist/core/entities/step.d.ts.map +1 -1
  160. package/dist/core/entities/step.js +2 -0
  161. package/dist/core/entities/step.js.map +1 -1
  162. package/dist/core/entities/workflow-run.d.ts +52 -4
  163. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  164. package/dist/core/entities/workflow-run.js +22 -3
  165. package/dist/core/entities/workflow-run.js.map +1 -1
  166. package/dist/core/workflow-run-tabs.d.ts +9 -0
  167. package/dist/core/workflow-run-tabs.d.ts.map +1 -0
  168. package/dist/core/workflow-run-tabs.js +3 -0
  169. package/dist/core/workflow-run-tabs.js.map +1 -0
  170. package/dist/core/workflow-run.d.ts +6 -6
  171. package/dist/core/workflow-run.d.ts.map +1 -1
  172. package/dist/core/workflow-run.js +3 -3
  173. package/dist/core/workflow-run.js.map +1 -1
  174. package/dist/feature.d.ts +7 -7
  175. package/dist/feature.js +5 -5
  176. package/dist/feature.js.map +1 -1
  177. package/dist/hooks/api/workflow-run-mapper.d.ts +4 -3
  178. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  179. package/dist/hooks/api/workflow-run-mapper.js +31 -3
  180. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  181. package/dist/hooks/api/workflow-runs.d.ts +4 -4
  182. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  183. package/dist/hooks/api/workflow-runs.js +12 -4
  184. package/dist/hooks/api/workflow-runs.js.map +1 -1
  185. package/dist/index.d.ts +4 -3
  186. package/dist/index.d.ts.map +1 -1
  187. package/dist/index.js +3 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/pages/project-workflows-page.js +46 -1
  190. package/dist/pages/project-workflows-page.js.map +1 -1
  191. package/dist/pages/workflow-run-detail-page.d.ts +11 -0
  192. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -0
  193. package/dist/pages/workflow-run-detail-page.js +52 -0
  194. package/dist/pages/workflow-run-detail-page.js.map +1 -0
  195. package/dist/pages/workflow-run-list-page.d.ts +10 -0
  196. package/dist/pages/workflow-run-list-page.d.ts.map +1 -0
  197. package/dist/pages/workflow-run-list-page.js +43 -0
  198. package/dist/pages/workflow-run-list-page.js.map +1 -0
  199. package/dist/routes/inputs.d.ts +94 -5
  200. package/dist/routes/inputs.d.ts.map +1 -1
  201. package/dist/routes/inputs.js +199 -13
  202. package/dist/routes/inputs.js.map +1 -1
  203. package/dist/routes/project-route.d.ts +6 -0
  204. package/dist/routes/project-route.d.ts.map +1 -0
  205. package/dist/routes/project-route.js +25 -0
  206. package/dist/routes/project-route.js.map +1 -0
  207. package/dist/routes/run-detail.d.ts.map +1 -1
  208. package/dist/routes/run-detail.js +12 -7
  209. package/dist/routes/run-detail.js.map +1 -1
  210. package/dist/routes/runs.d.ts.map +1 -1
  211. package/dist/routes/runs.js +11 -6
  212. package/dist/routes/runs.js.map +1 -1
  213. package/dist/routes/workflows.d.ts.map +1 -1
  214. package/dist/routes/workflows.js +6 -5
  215. package/dist/routes/workflows.js.map +1 -1
  216. package/dist/tsconfig.test.tsbuildinfo +1 -1
  217. package/package.json +9 -9
  218. package/src/components/job-graph/job-duration-format.ts +11 -2
  219. package/src/components/job-graph/job-duration-label.tsx +5 -1
  220. package/src/components/job-graph/job-graph-content.tsx +7 -4
  221. package/src/components/job-graph/job-graph-view.test.tsx +19 -8
  222. package/src/components/job-graph/job-graph-view.tsx +6 -3
  223. package/src/components/job-graph/job-graph.stories.tsx +24 -20
  224. package/src/components/job-graph/job-node.stories.tsx +11 -6
  225. package/src/components/job-graph/job-node.test.tsx +82 -12
  226. package/src/components/job-graph/job-node.tsx +22 -15
  227. package/src/components/job-graph/types.ts +5 -1
  228. package/src/components/step-list/step-list.stories.tsx +2 -3
  229. package/src/components/step-list/step-list.tsx +3 -2
  230. package/src/components/workflow-run-duration-label.tsx +1 -1
  231. package/src/components/workflow-run-list/job-status-strip.tsx +189 -0
  232. package/src/components/workflow-run-list/run-display.test.ts +169 -30
  233. package/src/components/workflow-run-list/run-display.ts +116 -9
  234. package/src/components/workflow-run-list/types.ts +21 -17
  235. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +100 -0
  236. package/src/components/workflow-run-list/workflow-run-filters.tsx +243 -0
  237. package/src/components/workflow-run-list/workflow-run-list-content.tsx +44 -16
  238. package/src/components/workflow-run-list/workflow-run-list-states.tsx +128 -27
  239. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +441 -104
  240. package/src/components/workflow-run-list/workflow-run-list-view.tsx +73 -43
  241. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +140 -20
  242. package/src/components/workflow-run-list/workflow-run-list.tsx +20 -10
  243. package/src/components/workflow-run-list/workflow-run-row.tsx +173 -106
  244. package/src/components/workflow-run-number-label.test.tsx +45 -0
  245. package/src/components/workflow-run-number-label.tsx +47 -0
  246. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +7 -8
  247. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +15 -14
  248. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +29 -38
  249. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +30 -24
  250. package/src/components/workflow-run-summary/workflow-run-summary.tsx +102 -77
  251. package/src/components/workflow-run-tabs/index.ts +5 -0
  252. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +69 -0
  253. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +97 -0
  254. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +11 -0
  255. package/src/components/workflow-run-tabs/run-jobs-list.tsx +151 -0
  256. package/src/components/workflow-run-tabs/run-tab-count.tsx +28 -0
  257. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +52 -0
  258. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +117 -0
  259. package/src/components/workflow-run-tabs/run-tab-strip.tsx +97 -0
  260. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +2 -3
  261. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +4 -4
  262. package/src/components/workflow-run-view/job-card.stories.tsx +11 -10
  263. package/src/components/workflow-run-view/job-card.test.tsx +115 -0
  264. package/src/components/workflow-run-view/job-card.tsx +74 -35
  265. package/src/components/workflow-run-view/job-execution-switcher.tsx +31 -9
  266. package/src/components/workflow-run-view/workflow-run-selection.test.ts +48 -0
  267. package/src/components/workflow-run-view/workflow-run-selection.ts +48 -1
  268. package/src/components/workflow-run-view/workflow-run-states.tsx +12 -1
  269. package/src/components/workflow-run-view/workflow-run-view.test.tsx +152 -94
  270. package/src/components/workflow-run-view/workflow-run-view.tsx +219 -95
  271. package/src/components/workflow-source-panel/index.ts +1 -0
  272. package/src/components/workflow-source-panel/workflow-source-content.tsx +84 -0
  273. package/src/components/workflow-source-panel/workflow-source-panel.tsx +14 -81
  274. package/src/components/workflow-status/status-visuals.test.ts +9 -0
  275. package/src/components/workflow-status/status-visuals.ts +6 -4
  276. package/src/components/workflow-status/workflow-status-icon.stories.tsx +10 -2
  277. package/src/components/workflow-status/workflow-status-icon.test.tsx +18 -0
  278. package/src/components/workflow-status/workflow-status-icon.tsx +17 -24
  279. package/src/core/entities/job-execution.ts +22 -0
  280. package/src/core/entities/job-status.test.ts +104 -0
  281. package/src/core/entities/job.ts +18 -3
  282. package/src/core/entities/step.ts +4 -0
  283. package/src/core/entities/workflow-run.ts +76 -7
  284. package/src/core/workflow-run-tabs.ts +8 -0
  285. package/src/core/workflow-run.test.ts +24 -9
  286. package/src/core/workflow-run.ts +17 -2
  287. package/src/feature.ts +5 -5
  288. package/src/hooks/api/workflow-run-mapper.ts +30 -3
  289. package/src/hooks/api/workflow-runs.test.tsx +2 -0
  290. package/src/hooks/api/workflow-runs.ts +12 -7
  291. package/src/index.ts +7 -2
  292. package/src/page-harness-budget.test.ts +1 -1
  293. package/src/pages/project-workflows-page.test.tsx +59 -5
  294. package/src/pages/project-workflows-page.tsx +34 -1
  295. package/src/pages/workflow-run-detail-page.tsx +72 -0
  296. package/src/pages/workflow-run-list-page.tsx +65 -0
  297. package/src/pages/workflow-run-pages.test.tsx +823 -0
  298. package/src/routes/inputs.test.ts +240 -12
  299. package/src/routes/inputs.ts +215 -11
  300. package/src/routes/project-route.tsx +25 -0
  301. package/src/routes/run-detail.tsx +15 -8
  302. package/src/routes/runs.tsx +14 -7
  303. package/src/routes/workflows.tsx +5 -4
  304. package/src/storybook-config.test.ts +27 -0
  305. package/test/fixtures/workflow-run.ts +76 -7
  306. package/test/pages.tsx +18 -8
  307. package/test/render.tsx +8 -2
  308. package/tsconfig.build.tsbuildinfo +1 -1
  309. package/tsconfig.test.json +7 -1
  310. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +0 -10
  311. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +0 -1
  312. package/dist/components/workflow-run-list/workflow-run-list-header.js +0 -55
  313. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +0 -1
  314. package/dist/pages/workflow-run-first-time-use.d.ts +0 -6
  315. package/dist/pages/workflow-run-first-time-use.d.ts.map +0 -1
  316. package/dist/pages/workflow-run-first-time-use.js +0 -17
  317. package/dist/pages/workflow-run-first-time-use.js.map +0 -1
  318. package/dist/pages/workflow-run-page.d.ts +0 -10
  319. package/dist/pages/workflow-run-page.d.ts.map +0 -1
  320. package/dist/pages/workflow-run-page.js +0 -90
  321. package/dist/pages/workflow-run-page.js.map +0 -1
  322. package/src/components/workflow-run-list/workflow-run-list-header.tsx +0 -53
  323. package/src/pages/workflow-run-first-time-use.tsx +0 -17
  324. package/src/pages/workflow-run-page.test.tsx +0 -519
  325. package/src/pages/workflow-run-page.tsx +0 -94
@@ -0,0 +1,823 @@
1
+ import type {
2
+ WorkflowRunDetailResponseDto,
3
+ WorkflowRunResponseDto,
4
+ } from '@shipfox/api-workflows-dto';
5
+ import {configureApiClient} from '@shipfox/client-api';
6
+ import {act, screen, waitFor} from '@testing-library/react';
7
+ import userEvent from '@testing-library/user-event';
8
+ import {inlineLogBody, outputLine} from '#test/fixtures/logs.js';
9
+ import {
10
+ workflowJobDto,
11
+ workflowJobExecutionDto,
12
+ workflowRunDetailDto,
13
+ workflowRunDto,
14
+ workflowStepAttemptDto,
15
+ workflowStepDto,
16
+ } from '#test/fixtures/workflow-run.js';
17
+ import {jsonResponse, PROJECT_TEST_WSLUG, renderProjectPage} from '#test/pages.js';
18
+ import {WorkflowRunDetailPage} from './workflow-run-detail-page.js';
19
+ import {WorkflowRunsPage} from './workflow-run-list-page.js';
20
+
21
+ const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
22
+ const DEFINITION_ID = '55555555-5555-4555-8555-555555555555';
23
+ const RUN_ID = '66666666-6666-4666-8666-666666666666';
24
+ const SECOND_RUN_ID = '66666666-6666-4666-8666-000000000002';
25
+ const OLDER_RUN_ID = '66666666-6666-4666-8666-000000000003';
26
+ const BUILD_JOB_ID = '77777777-7777-4777-8777-777777777777';
27
+ const BUILD_STEP_ID = '99999999-9999-4999-8999-000000000000';
28
+ const BUILD_ATTEMPT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000000';
29
+ const DEPLOY_JOB_ID = '88888888-8888-4888-8888-888888888888';
30
+ const DEPLOY_STEP_ID = '99999999-9999-4999-8999-999999999999';
31
+ const DEPLOY_RETRY_STEP_ID = '99999999-9999-4999-8999-000000000003';
32
+ const DEPLOY_ATTEMPT_ONE_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000001';
33
+ const DEPLOY_ATTEMPT_TWO_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000002';
34
+ const DEPLOY_EXECUTION_ONE_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-000000000001';
35
+ const DEPLOY_EXECUTION_TWO_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-000000000002';
36
+ const SMOKE_WEB_RE = /smoke-web/u;
37
+ const DEPLOY_WEB_RE = /deploy-web/u;
38
+ const OLDER_RUN_RE = /older-run/u;
39
+ const INTEGRATION_TESTS_RE = /integration-tests/u;
40
+ const BUILD_IMAGE_RE = /build-image/u;
41
+ const STATUS_FILTER_RE = /^Status\b.*filter$/u;
42
+ const EXECUTION_ONE_MENU_ITEM = /Execution #1: deploy review #1/u;
43
+ const JOBS_TAB_NAME = /^Jobs/u;
44
+ const BUILD_JOB_BUTTON_NAME = 'build, Succeeded';
45
+ const DEPLOY_JOB_BUTTON_NAME = 'deploy, Running';
46
+ const LIVE_DEPLOY_JOB_BUTTON_NAME = /^deploy, Running, running /u;
47
+ const RUN_DETAIL_PATH_RE = /^\/workflows\/runs\/([^/]+)$/u;
48
+ const RUN_OVERRIDES = {
49
+ id: RUN_ID,
50
+ project_id: PROJECT_ID,
51
+ definition_id: DEFINITION_ID,
52
+ trigger_payload: {source: 'manual', event: 'fire'},
53
+ created_at: '2026-05-07T01:01:00.000Z',
54
+ updated_at: '2026-05-07T01:02:00.000Z',
55
+ } satisfies Partial<WorkflowRunResponseDto>;
56
+ const SECOND_RUN_OVERRIDES = {
57
+ ...RUN_OVERRIDES,
58
+ id: SECOND_RUN_ID,
59
+ name: 'smoke-web',
60
+ } satisfies Partial<WorkflowRunResponseDto>;
61
+ const OLDER_RUN_OVERRIDES = {
62
+ ...RUN_OVERRIDES,
63
+ id: OLDER_RUN_ID,
64
+ name: 'older-run',
65
+ } satisfies Partial<WorkflowRunResponseDto>;
66
+
67
+ describe('WorkflowRunPages', () => {
68
+ test('renders the list route without mounting run detail', async () => {
69
+ configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
70
+
71
+ renderRunsPath();
72
+
73
+ expect(await screen.findByLabelText('Loading runs')).toBeInTheDocument();
74
+ expect(screen.getByLabelText('Workflow runs')).toBeInTheDocument();
75
+ expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
76
+ });
77
+
78
+ test('keeps the list route and filters in place instead of redirecting to a run', async () => {
79
+ configureApiClient({fetchImpl: createRunsListFetch()});
80
+
81
+ const {router} = renderRunsPath('?search=deploy-web&status=running');
82
+
83
+ expect(await screen.findByRole('link', {name: DEPLOY_WEB_RE})).toBeInTheDocument();
84
+ expect(router.state.location.pathname).toBe(`/w/${PROJECT_TEST_WSLUG}/p/project/runs`);
85
+ expect(currentSearch(router)).toMatchObject({search: 'deploy-web', status: 'running'});
86
+ expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
87
+ });
88
+
89
+ test('clearing filters on the list route resets search and status in the URL', async () => {
90
+ const user = userEvent.setup();
91
+ configureApiClient({fetchImpl: createRunsListFetch()});
92
+
93
+ const {router} = renderRunsPath('?search=no-such-run&status=failed');
94
+
95
+ expect(await screen.findByText('No matching runs')).toBeInTheDocument();
96
+
97
+ await user.click(screen.getByRole('button', {name: 'Clear filters'}));
98
+
99
+ await waitFor(() => {
100
+ expect(currentSearch(router).search).toBeUndefined();
101
+ });
102
+ expect(currentSearch(router).status).toBeUndefined();
103
+ expect(await screen.findByRole('link', {name: DEPLOY_WEB_RE})).toBeInTheDocument();
104
+ });
105
+
106
+ test('honors a deep link that repeats a filter key', async () => {
107
+ configureApiClient({fetchImpl: createMixedStatusRunsFetch()});
108
+
109
+ renderRunsPath('?status=failed&status=running');
110
+
111
+ expect(await screen.findByRole('link', {name: DEPLOY_WEB_RE})).toBeInTheDocument();
112
+ expect(await screen.findByRole('link', {name: INTEGRATION_TESTS_RE})).toBeInTheDocument();
113
+ expect(screen.queryByRole('link', {name: BUILD_IMAGE_RE})).not.toBeInTheDocument();
114
+ });
115
+
116
+ test('writes a multi-select as repeated keys rather than one joined value', async () => {
117
+ const user = userEvent.setup();
118
+ configureApiClient({fetchImpl: createMixedStatusRunsFetch()});
119
+
120
+ const {router} = renderRunsPath('?status=failed');
121
+
122
+ await user.click(await screen.findByRole('button', {name: STATUS_FILTER_RE}));
123
+ await user.click(await screen.findByRole('menuitemcheckbox', {name: 'Running'}));
124
+
125
+ await waitFor(() => {
126
+ expect(currentSearch(router).status).toEqual(['failed', 'running']);
127
+ });
128
+ expect(router.state.location.searchStr).toBe('?status=failed&status=running');
129
+ });
130
+
131
+ test('replaces history on a filter change so back leaves the list', async () => {
132
+ const user = userEvent.setup();
133
+ configureApiClient({fetchImpl: createMixedStatusRunsFetch()});
134
+
135
+ const {router} = renderRunsPath();
136
+ const initialLength = router.history.length;
137
+
138
+ await user.type(await screen.findByLabelText('Search runs'), 'deploy');
139
+
140
+ await waitFor(() => {
141
+ expect(currentSearch(router).search).toBe('deploy');
142
+ });
143
+ expect(router.history.length).toBe(initialLength);
144
+ });
145
+
146
+ test('loads older pages before reporting that a search has no matches', async () => {
147
+ const user = userEvent.setup();
148
+ const fetchImpl = createPaginatedRunsFetch();
149
+ configureApiClient({fetchImpl});
150
+
151
+ renderRunsPath('?search=older-run');
152
+
153
+ expect(await screen.findByText('No matches in loaded history')).toBeInTheDocument();
154
+ await user.click(screen.getByRole('button', {name: 'Load more runs'}));
155
+
156
+ expect(await screen.findByRole('link', {name: OLDER_RUN_RE})).toBeInTheDocument();
157
+ expect(
158
+ fetchImpl.mock.calls.some((call) => {
159
+ const url = new URL(requestInputUrl(call[0]));
160
+ return url.searchParams.get('cursor') === 'cursor-2';
161
+ }),
162
+ ).toBe(true);
163
+ });
164
+
165
+ test('shows the list empty state without mounting run detail', async () => {
166
+ configureApiClient({fetchImpl: createEmptyRunsFetch()});
167
+
168
+ renderRunsPath();
169
+
170
+ expect(await screen.findByText('No runs yet')).toBeInTheDocument();
171
+ expect(screen.getByLabelText('Workflow runs')).toBeInTheDocument();
172
+ expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
173
+ });
174
+
175
+ test('renders the detail route without mounting the run list', async () => {
176
+ configureApiClient({fetchImpl: createRunDetailFetch()});
177
+
178
+ renderRunPath('?tab=jobs');
179
+
180
+ expect(await screen.findByRole('button', {name: DEPLOY_JOB_BUTTON_NAME})).toBeInTheDocument();
181
+ expect(screen.getByRole('link', {name: 'Runs'})).toHaveAttribute(
182
+ 'href',
183
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs`,
184
+ );
185
+ expect(screen.queryByLabelText('Workflow runs')).not.toBeInTheDocument();
186
+ });
187
+
188
+ test('back to runs preserves list filters and drops run detail state', async () => {
189
+ configureApiClient({fetchImpl: createRunDetailFetch()});
190
+
191
+ renderRunPath(`?search=deploy-web&status=running&tab=jobs&job=${DEPLOY_JOB_ID}`);
192
+
193
+ expect(await screen.findByRole('link', {name: 'Runs'})).toHaveAttribute(
194
+ 'href',
195
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs?search=deploy-web&status=running`,
196
+ );
197
+ });
198
+
199
+ test('defaults the detail route to the Summary tab', async () => {
200
+ configureApiClient({fetchImpl: createRunDetailFetch()});
201
+
202
+ const {router} = renderRunPath();
203
+
204
+ expect(await screen.findByRole('region', {name: 'deploy-web'})).toBeInTheDocument();
205
+ const summaryTab = screen.getByRole('tab', {name: 'Summary'});
206
+ expect(summaryTab).toHaveAttribute('aria-selected', 'true');
207
+ expect(summaryTab).toHaveAttribute('aria-controls', screen.getByRole('tabpanel').id);
208
+ expect(screen.queryByRole('list', {name: 'Run jobs'})).not.toBeInTheDocument();
209
+ expect(currentSearch(router).tab).toBeUndefined();
210
+ });
211
+
212
+ test('opens legacy selection-only detail URLs on the Jobs tab', async () => {
213
+ configureApiClient({fetchImpl: createRunDetailFetch()});
214
+
215
+ renderRunPath(`?job=${DEPLOY_JOB_ID}`);
216
+
217
+ expect(await screen.findByRole('list', {name: 'Run jobs'})).toBeInTheDocument();
218
+ expect(screen.getByRole('tab', {name: JOBS_TAB_NAME})).toHaveAttribute('aria-selected', 'true');
219
+ });
220
+
221
+ test('opens a legacy job execution URL with its owning job selected', async () => {
222
+ configureApiClient({
223
+ fetchImpl: createRunDetailFetch({details: {[RUN_ID]: retryRunDetailDto()}}),
224
+ });
225
+
226
+ renderRunPath(`?jobExecution=${DEPLOY_EXECUTION_TWO_ID}`);
227
+
228
+ expect(await screen.findByRole('region', {name: 'deploy review #2'})).toBeInTheDocument();
229
+ });
230
+
231
+ test('uses the derived execution status in a live job duration label', async () => {
232
+ configureApiClient({
233
+ fetchImpl: createRunDetailFetch({
234
+ details: {
235
+ [RUN_ID]: defaultRunDetailDto({
236
+ jobs: [
237
+ workflowJobDto({
238
+ id: DEPLOY_JOB_ID,
239
+ run_attempt_id: RUN_ID,
240
+ name: 'deploy',
241
+ status: 'running',
242
+ job_executions: [
243
+ workflowJobExecutionDto({
244
+ job_id: DEPLOY_JOB_ID,
245
+ status: 'pending',
246
+ started_at: '2026-08-04T00:00:00.000Z',
247
+ steps: [workflowStepDto({status: 'running'})],
248
+ }),
249
+ ],
250
+ }),
251
+ ],
252
+ }),
253
+ },
254
+ }),
255
+ });
256
+
257
+ renderRunPath(`?tab=jobs&job=${DEPLOY_JOB_ID}`);
258
+
259
+ expect(
260
+ await screen.findByRole('button', {name: LIVE_DEPLOY_JOB_BUTTON_NAME}),
261
+ ).toBeInTheDocument();
262
+ });
263
+
264
+ test('pushes tab changes and restores them with browser history', async () => {
265
+ const user = userEvent.setup();
266
+ configureApiClient({fetchImpl: createRunDetailFetch()});
267
+
268
+ const {router} = renderRunPath();
269
+ const initialHistoryLength = router.history.length;
270
+
271
+ await user.click(await screen.findByRole('tab', {name: JOBS_TAB_NAME}));
272
+
273
+ await waitFor(() => expect(currentSearch(router).tab).toBe('jobs'));
274
+ expect(router.history.length).toBe(initialHistoryLength + 1);
275
+ expect(screen.getByRole('tab', {name: JOBS_TAB_NAME})).toHaveAttribute('aria-selected', 'true');
276
+ expect(screen.getByRole('list', {name: 'Run jobs'})).toBeInTheDocument();
277
+
278
+ await user.click(screen.getByRole('tab', {name: 'Summary'}));
279
+
280
+ await waitFor(() => expect(currentSearch(router).tab).toBeUndefined());
281
+ expect(router.history.length).toBe(initialHistoryLength + 2);
282
+
283
+ await act(() => {
284
+ router.history.back();
285
+ });
286
+ await waitFor(() => expect(currentSearch(router).tab).toBe('jobs'));
287
+ expect(screen.getByRole('list', {name: 'Run jobs'})).toBeInTheDocument();
288
+ });
289
+
290
+ test('opens the Jobs tab and selects a job from the Summary graph', async () => {
291
+ const user = userEvent.setup();
292
+ configureApiClient({fetchImpl: createRunDetailFetch()});
293
+
294
+ const {router} = renderRunPath();
295
+ const initialHistoryLength = router.history.length;
296
+
297
+ await user.click(await screen.findByRole('button', {name: 'deploy, Running'}));
298
+
299
+ await waitFor(() => {
300
+ expect(currentSearch(router)).toMatchObject({tab: 'jobs', job: DEPLOY_JOB_ID});
301
+ });
302
+ expect(router.history.length).toBe(initialHistoryLength + 1);
303
+ expect(screen.getByRole('button', {name: 'deploy, Running'})).toHaveAttribute(
304
+ 'aria-expanded',
305
+ 'true',
306
+ );
307
+ expect(screen.getByRole('button', {name: 'deploy, Running'})).toHaveAttribute(
308
+ 'aria-controls',
309
+ `job-card-${DEPLOY_JOB_ID}`,
310
+ );
311
+ expect(screen.getByRole('button', {name: BUILD_JOB_BUTTON_NAME})).not.toHaveAttribute(
312
+ 'aria-controls',
313
+ );
314
+ });
315
+
316
+ test('preserves an explicit Summary tab with a selected job', async () => {
317
+ const user = userEvent.setup();
318
+ configureApiClient({fetchImpl: createRunDetailFetch()});
319
+
320
+ const {router} = renderRunPath(`?tab=jobs&job=${DEPLOY_JOB_ID}`);
321
+
322
+ await screen.findByRole('button', {name: DEPLOY_JOB_BUTTON_NAME});
323
+ await user.click(screen.getByRole('tab', {name: 'Summary'}));
324
+
325
+ await waitFor(() => {
326
+ expect(currentSearch(router)).toMatchObject({tab: 'summary', job: DEPLOY_JOB_ID});
327
+ });
328
+ expect(screen.getByRole('tab', {name: 'Summary'})).toHaveAttribute('aria-selected', 'true');
329
+ expect(screen.queryByRole('list', {name: 'Run jobs'})).not.toBeInTheDocument();
330
+ });
331
+
332
+ test('keeps Summary active while keyboard navigation moves graph focus', async () => {
333
+ const user = userEvent.setup();
334
+ configureApiClient({fetchImpl: createRunDetailFetch()});
335
+
336
+ const {router} = renderRunPath();
337
+ const build = await screen.findByRole('button', {name: BUILD_JOB_BUTTON_NAME});
338
+ const deploy = screen.getByRole('button', {name: DEPLOY_JOB_BUTTON_NAME});
339
+
340
+ build.focus();
341
+ await user.keyboard('{ArrowRight}');
342
+
343
+ expect(deploy).toHaveFocus();
344
+ expect(deploy).toHaveAttribute('aria-pressed', 'true');
345
+ expect(screen.getByRole('tab', {name: 'Summary'})).toHaveAttribute('aria-selected', 'true');
346
+ expect(screen.queryByRole('list', {name: 'Run jobs'})).not.toBeInTheDocument();
347
+ expect(currentSearch(router).job).toBeUndefined();
348
+ });
349
+
350
+ test('restores a deep-linked job and exact attempt after data loads', async () => {
351
+ configureApiClient({fetchImpl: createRunDetailFetch()});
352
+
353
+ renderRunPath(
354
+ `?tab=jobs&job=${BUILD_JOB_ID}&step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}`,
355
+ );
356
+
357
+ const deployJob = await screen.findByRole('button', {name: DEPLOY_JOB_BUTTON_NAME});
358
+ const deployAttempt = await screen.findByRole('button', {
359
+ name: 'deploy, Running, attempt 2',
360
+ });
361
+
362
+ expect(deployJob).toHaveAttribute('aria-expanded', 'true');
363
+ expect(deployAttempt).toHaveAttribute('aria-expanded', 'true');
364
+ expect(await screen.findByText('attempt two log')).toBeInTheDocument();
365
+ });
366
+
367
+ test('selecting a job writes job search state and clears stale step state', async () => {
368
+ const user = userEvent.setup();
369
+ configureApiClient({fetchImpl: createRunDetailFetch()});
370
+ const {router} = renderRunPath(
371
+ `?tab=jobs&step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}&runAttempt=1`,
372
+ );
373
+ const initialHistoryLength = router.history.length;
374
+
375
+ await user.click(await screen.findByRole('button', {name: BUILD_JOB_BUTTON_NAME}));
376
+
377
+ await waitFor(() => {
378
+ expect(currentSearch(router)).toMatchObject({job: BUILD_JOB_ID});
379
+ });
380
+ expect(currentSearch(router).tab).toBe('jobs');
381
+ expect(router.history.length).toBe(initialHistoryLength);
382
+ expect(currentSearch(router).step).toBeUndefined();
383
+ expect(currentSearch(router).stepAttempt).toBeUndefined();
384
+ expect(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'})).toHaveAttribute(
385
+ 'aria-expanded',
386
+ 'false',
387
+ );
388
+ });
389
+
390
+ test('selecting an attempt writes job, step, and attempt search state', async () => {
391
+ const user = userEvent.setup();
392
+ configureApiClient({fetchImpl: createRunDetailFetch()});
393
+ const {router} = renderRunPath(`?tab=jobs&job=${DEPLOY_JOB_ID}`);
394
+
395
+ await user.click(await screen.findByRole('button', {name: 'deploy, Running, attempt 2'}));
396
+
397
+ await waitFor(() => {
398
+ expect(currentSearch(router)).toMatchObject({
399
+ job: DEPLOY_JOB_ID,
400
+ step: DEPLOY_STEP_ID,
401
+ stepAttempt: DEPLOY_ATTEMPT_TWO_ID,
402
+ });
403
+ });
404
+ });
405
+
406
+ test('selecting a listening execution writes job execution search state and scopes step selection', async () => {
407
+ const user = userEvent.setup();
408
+ configureApiClient({
409
+ fetchImpl: createRunDetailFetch({details: {[RUN_ID]: retryRunDetailDto()}}),
410
+ });
411
+ const {router} = renderRunPath(`?tab=jobs&job=${DEPLOY_JOB_ID}`);
412
+
413
+ await user.click(
414
+ await screen.findByRole('button', {
415
+ name: 'Switch job execution, currently execution 2: deploy review #2',
416
+ }),
417
+ );
418
+ await user.click(
419
+ screen.getByRole('menuitem', {
420
+ name: EXECUTION_ONE_MENU_ITEM,
421
+ }),
422
+ );
423
+
424
+ await waitFor(() => {
425
+ expect(currentSearch(router)).toMatchObject({
426
+ job: DEPLOY_JOB_ID,
427
+ jobExecution: DEPLOY_EXECUTION_ONE_ID,
428
+ });
429
+ });
430
+ expect(screen.getByRole('button', {name: 'deploy, Failed, attempt 1'})).toBeInTheDocument();
431
+ expect(screen.getAllByText('deploy review #1')).not.toHaveLength(0);
432
+
433
+ await user.click(screen.getByRole('button', {name: 'deploy, Failed, attempt 1'}));
434
+
435
+ await waitFor(() => {
436
+ expect(currentSearch(router)).toMatchObject({
437
+ job: DEPLOY_JOB_ID,
438
+ jobExecution: DEPLOY_EXECUTION_ONE_ID,
439
+ step: DEPLOY_STEP_ID,
440
+ stepAttempt: DEPLOY_ATTEMPT_ONE_ID,
441
+ });
442
+ });
443
+ expect(await screen.findByText('attempt one log')).toBeInTheDocument();
444
+ });
445
+
446
+ test('collapsing an attempt removes step and attempt while preserving job', async () => {
447
+ const user = userEvent.setup();
448
+ configureApiClient({fetchImpl: createRunDetailFetch()});
449
+ const {router} = renderRunPath(
450
+ `?tab=jobs&step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}`,
451
+ );
452
+
453
+ const deployAttempt = await screen.findByRole('button', {
454
+ name: 'deploy, Running, attempt 2',
455
+ });
456
+ await user.click(deployAttempt);
457
+
458
+ await waitFor(() => {
459
+ expect(currentSearch(router)).toMatchObject({job: DEPLOY_JOB_ID});
460
+ });
461
+ expect(currentSearch(router).step).toBeUndefined();
462
+ expect(currentSearch(router).stepAttempt).toBeUndefined();
463
+ expect(deployAttempt).toHaveAttribute('aria-expanded', 'false');
464
+ });
465
+
466
+ test('run list links navigate to detail and back restores list filters', async () => {
467
+ const user = userEvent.setup();
468
+ configureApiClient({
469
+ fetchImpl: createRunDetailFetch({
470
+ runs: [workflowRunDto(RUN_OVERRIDES), workflowRunDto(SECOND_RUN_OVERRIDES)],
471
+ details: {
472
+ [RUN_ID]: defaultRunDetailDto(),
473
+ [SECOND_RUN_ID]: workflowRunDetailDto({...SECOND_RUN_OVERRIDES, jobs: []}),
474
+ },
475
+ }),
476
+ });
477
+ const {router} = renderRunsPath('?search=smoke&status=running');
478
+
479
+ await user.click(await screen.findByRole('link', {name: SMOKE_WEB_RE}));
480
+
481
+ await waitFor(() => {
482
+ expect(router.state.location.pathname).toBe(
483
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs/${SECOND_RUN_ID}`,
484
+ );
485
+ });
486
+ expect(screen.queryByLabelText('Workflow runs')).not.toBeInTheDocument();
487
+ expect(currentSearch(router)).toMatchObject({search: 'smoke', status: 'running'});
488
+
489
+ await act(() => {
490
+ router.history.back();
491
+ });
492
+
493
+ await waitFor(() => {
494
+ expect(router.state.location.pathname).toBe(`/w/${PROJECT_TEST_WSLUG}/p/project/runs`);
495
+ });
496
+ expect(currentSearch(router)).toMatchObject({search: 'smoke', status: 'running'});
497
+ expect(screen.getByLabelText('Workflow runs')).toBeInTheDocument();
498
+ expect(screen.getByRole('link', {name: SMOKE_WEB_RE})).toBeInTheDocument();
499
+ });
500
+ });
501
+
502
+ function renderRunsPath(search = '') {
503
+ return renderProjectPage(
504
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs${search}`,
505
+ ({workflowRunId, search}) =>
506
+ workflowRunId ? (
507
+ <WorkflowRunDetailPage
508
+ projectId={PROJECT_ID}
509
+ workspaceSlug={PROJECT_TEST_WSLUG}
510
+ projectSlug="project"
511
+ workflowRunId={workflowRunId}
512
+ search={search}
513
+ />
514
+ ) : (
515
+ <WorkflowRunsPage
516
+ projectId={PROJECT_ID}
517
+ workspaceSlug={PROJECT_TEST_WSLUG}
518
+ projectSlug="project"
519
+ search={search}
520
+ />
521
+ ),
522
+ );
523
+ }
524
+
525
+ function renderRunPath(search = '') {
526
+ return renderProjectPage(
527
+ `/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}${search}`,
528
+ ({workflowRunId, search}) => (
529
+ <WorkflowRunDetailPage
530
+ projectId={PROJECT_ID}
531
+ workspaceSlug={PROJECT_TEST_WSLUG}
532
+ projectSlug="project"
533
+ workflowRunId={workflowRunId}
534
+ search={search}
535
+ />
536
+ ),
537
+ );
538
+ }
539
+
540
+ function createRunsListFetch() {
541
+ return vi.fn((input: RequestInfo | URL) => {
542
+ const url = new URL(requestInputUrl(input));
543
+
544
+ if (url.pathname === '/workflows/runs') {
545
+ return Promise.resolve(
546
+ jsonResponse({
547
+ runs: [workflowRunDto(RUN_OVERRIDES)],
548
+ next_cursor: null,
549
+ filtered_total_count: 1,
550
+ }),
551
+ );
552
+ }
553
+ if (url.pathname === `/workflows/runs/${RUN_ID}`) {
554
+ return Promise.resolve(jsonResponse(workflowRunDetailDto({...RUN_OVERRIDES, jobs: []})));
555
+ }
556
+
557
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
558
+ });
559
+ }
560
+
561
+ function createMixedStatusRunsFetch() {
562
+ const runs = [
563
+ workflowRunDto({...RUN_OVERRIDES, status: 'running'}),
564
+ workflowRunDto({
565
+ ...RUN_OVERRIDES,
566
+ id: SECOND_RUN_ID,
567
+ name: 'integration-tests',
568
+ workflow_name: 'integration-tests',
569
+ status: 'failed',
570
+ }),
571
+ workflowRunDto({
572
+ ...RUN_OVERRIDES,
573
+ id: OLDER_RUN_ID,
574
+ name: 'build-image',
575
+ workflow_name: 'build-image',
576
+ status: 'succeeded',
577
+ }),
578
+ ];
579
+
580
+ return vi.fn((input: RequestInfo | URL) => {
581
+ const url = new URL(requestInputUrl(input));
582
+
583
+ if (url.pathname === '/workflows/runs') {
584
+ return Promise.resolve(jsonResponse({runs, next_cursor: null, filtered_total_count: 3}));
585
+ }
586
+
587
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
588
+ });
589
+ }
590
+
591
+ function createPaginatedRunsFetch() {
592
+ return vi.fn((input: RequestInfo | URL) => {
593
+ const url = new URL(requestInputUrl(input));
594
+
595
+ if (url.pathname === '/workflows/runs') {
596
+ const isSecondPage = url.searchParams.get('cursor') === 'cursor-2';
597
+ return Promise.resolve(
598
+ jsonResponse({
599
+ runs: [workflowRunDto(isSecondPage ? OLDER_RUN_OVERRIDES : RUN_OVERRIDES)],
600
+ next_cursor: isSecondPage ? null : 'cursor-2',
601
+ filtered_total_count: isSecondPage ? null : 2,
602
+ }),
603
+ );
604
+ }
605
+
606
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
607
+ });
608
+ }
609
+
610
+ function createRunDetailFetch({
611
+ runs = [workflowRunDto(RUN_OVERRIDES)],
612
+ details = {[RUN_ID]: defaultRunDetailDto()},
613
+ }: {
614
+ runs?: WorkflowRunResponseDto[];
615
+ details?: Record<string, WorkflowRunDetailResponseDto>;
616
+ } = {}) {
617
+ return vi.fn((input: RequestInfo | URL) => {
618
+ const url = new URL(requestInputUrl(input));
619
+
620
+ if (url.pathname === '/workflows/runs') {
621
+ return Promise.resolve(
622
+ jsonResponse({runs, next_cursor: null, filtered_total_count: runs.length}),
623
+ );
624
+ }
625
+
626
+ const runMatch = url.pathname.match(RUN_DETAIL_PATH_RE);
627
+ if (runMatch?.[1] && details[runMatch[1]]) {
628
+ return Promise.resolve(jsonResponse(details[runMatch[1]]));
629
+ }
630
+
631
+ if (url.pathname === `/steps/${DEPLOY_STEP_ID}/attempts/1/logs`) {
632
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('attempt one log\n'), 1)));
633
+ }
634
+ if (url.pathname === `/steps/${DEPLOY_STEP_ID}/attempts/2/logs`) {
635
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('attempt two log\n'), 1)));
636
+ }
637
+ if (url.pathname === `/steps/${DEPLOY_RETRY_STEP_ID}/attempts/1/logs`) {
638
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('retry attempt log\n'), 1)));
639
+ }
640
+ if (url.pathname === `/steps/${BUILD_STEP_ID}/attempts/1/logs`) {
641
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('build log\n'), 1)));
642
+ }
643
+
644
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
645
+ });
646
+ }
647
+
648
+ function createEmptyRunsFetch() {
649
+ return vi.fn((input: RequestInfo | URL) => {
650
+ const url = new URL(requestInputUrl(input));
651
+
652
+ if (url.pathname === '/workflows/runs') {
653
+ return Promise.resolve(jsonResponse({runs: [], next_cursor: null, filtered_total_count: 0}));
654
+ }
655
+
656
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
657
+ });
658
+ }
659
+
660
+ function requestInputUrl(input: RequestInfo | URL) {
661
+ if (input instanceof Request) return input.url;
662
+ return String(input);
663
+ }
664
+
665
+ function defaultRunDetailDto(
666
+ overrides: Partial<WorkflowRunDetailResponseDto> = {},
667
+ ): WorkflowRunDetailResponseDto {
668
+ return workflowRunDetailDto({
669
+ ...RUN_OVERRIDES,
670
+ jobs: [
671
+ workflowJobDto({
672
+ id: BUILD_JOB_ID,
673
+ run_attempt_id: RUN_ID,
674
+ name: 'build',
675
+ status: 'succeeded',
676
+ steps: [
677
+ workflowStepDto({
678
+ id: BUILD_STEP_ID,
679
+ key: 'checkout',
680
+ name: 'checkout',
681
+ status: 'succeeded',
682
+ current_attempt: 1,
683
+ attempts: [
684
+ workflowStepAttemptDto({
685
+ id: BUILD_ATTEMPT_ID,
686
+ step_id: BUILD_STEP_ID,
687
+ status: 'succeeded',
688
+ }),
689
+ ],
690
+ }),
691
+ ],
692
+ }),
693
+ workflowJobDto({
694
+ id: DEPLOY_JOB_ID,
695
+ run_attempt_id: RUN_ID,
696
+ name: 'deploy',
697
+ status: 'running',
698
+ position: 1,
699
+ dependencies: ['build'],
700
+ steps: [
701
+ workflowStepDto({
702
+ id: DEPLOY_STEP_ID,
703
+ key: 'deploy',
704
+ name: 'deploy',
705
+ status: 'running',
706
+ current_attempt: 2,
707
+ attempts: [
708
+ workflowStepAttemptDto({
709
+ id: DEPLOY_ATTEMPT_ONE_ID,
710
+ step_id: DEPLOY_STEP_ID,
711
+ attempt: 1,
712
+ execution_order: 1,
713
+ status: 'failed',
714
+ exit_code: 1,
715
+ }),
716
+ workflowStepAttemptDto({
717
+ id: DEPLOY_ATTEMPT_TWO_ID,
718
+ step_id: DEPLOY_STEP_ID,
719
+ attempt: 2,
720
+ execution_order: 2,
721
+ status: 'running',
722
+ exit_code: null,
723
+ finished_at: null,
724
+ }),
725
+ ],
726
+ }),
727
+ ],
728
+ }),
729
+ ],
730
+ ...overrides,
731
+ });
732
+ }
733
+
734
+ function retryRunDetailDto(): WorkflowRunDetailResponseDto {
735
+ return defaultRunDetailDto({
736
+ jobs: [
737
+ workflowJobDto({
738
+ id: BUILD_JOB_ID,
739
+ run_attempt_id: RUN_ID,
740
+ name: 'build',
741
+ status: 'succeeded',
742
+ steps: [
743
+ workflowStepDto({
744
+ id: BUILD_STEP_ID,
745
+ name: 'checkout',
746
+ status: 'succeeded',
747
+ attempts: [
748
+ workflowStepAttemptDto({
749
+ id: BUILD_ATTEMPT_ID,
750
+ step_id: BUILD_STEP_ID,
751
+ status: 'succeeded',
752
+ }),
753
+ ],
754
+ }),
755
+ ],
756
+ }),
757
+ workflowJobDto({
758
+ id: DEPLOY_JOB_ID,
759
+ run_attempt_id: RUN_ID,
760
+ name: 'deploy',
761
+ mode: 'listening',
762
+ status: 'running',
763
+ listener_status: 'listening',
764
+ position: 1,
765
+ dependencies: ['build'],
766
+ job_executions: [
767
+ workflowJobExecutionDto({
768
+ id: DEPLOY_EXECUTION_ONE_ID,
769
+ job_id: DEPLOY_JOB_ID,
770
+ sequence: 1,
771
+ name: 'deploy review #1',
772
+ status: 'failed',
773
+ started_at: '2026-05-07T01:01:00.000Z',
774
+ finished_at: '2026-05-07T01:02:00.000Z',
775
+ steps: [
776
+ workflowStepDto({
777
+ id: DEPLOY_STEP_ID,
778
+ name: 'deploy',
779
+ status: 'failed',
780
+ attempts: [
781
+ workflowStepAttemptDto({
782
+ id: DEPLOY_ATTEMPT_ONE_ID,
783
+ step_id: DEPLOY_STEP_ID,
784
+ status: 'failed',
785
+ exit_code: 1,
786
+ }),
787
+ ],
788
+ }),
789
+ ],
790
+ }),
791
+ workflowJobExecutionDto({
792
+ id: DEPLOY_EXECUTION_TWO_ID,
793
+ job_id: DEPLOY_JOB_ID,
794
+ sequence: 2,
795
+ name: 'deploy review #2',
796
+ status: 'running',
797
+ started_at: '2026-05-07T01:03:00.000Z',
798
+ steps: [
799
+ workflowStepDto({
800
+ id: DEPLOY_RETRY_STEP_ID,
801
+ name: 'deploy retry',
802
+ status: 'running',
803
+ attempts: [
804
+ workflowStepAttemptDto({
805
+ id: DEPLOY_ATTEMPT_TWO_ID,
806
+ step_id: DEPLOY_RETRY_STEP_ID,
807
+ status: 'running',
808
+ exit_code: null,
809
+ finished_at: null,
810
+ }),
811
+ ],
812
+ }),
813
+ ],
814
+ }),
815
+ ],
816
+ }),
817
+ ],
818
+ });
819
+ }
820
+
821
+ function currentSearch({state}: ReturnType<typeof renderRunPath>['router']) {
822
+ return state.location.search as Record<string, unknown>;
823
+ }