@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
@@ -1,15 +1,23 @@
1
- import {screen} from '@testing-library/react';
1
+ import type {JobStatusDto} from '@shipfox/api-workflows-dto';
2
+ import {screen, within} from '@testing-library/react';
2
3
  import userEvent from '@testing-library/user-event';
3
4
  import type {WorkflowRunListItem, WorkflowRunStatus} from '#core/workflow-run.js';
4
- import {workflowRunListItem} from '#test/fixtures/workflow-run.js';
5
+ import {
6
+ workflowRunJobsFixture,
7
+ workflowRunJobsOfStatus,
8
+ workflowRunListItem,
9
+ } from '#test/fixtures/workflow-run.js';
5
10
  import {renderWithRouter} from '#test/render.js';
6
- import type {WorkflowRunListQuery} from './types.js';
11
+ import type {WorkflowRunListQuery, WorkflowRunListViewProps} from './types.js';
7
12
  import {WorkflowRunListView} from './workflow-run-list-view.js';
8
13
 
9
- const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
10
14
  const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
15
+ const JOB_STRIP_LABEL_RE = /jobs:/u;
16
+ const JOB_SUMMARY_NAME_RE = /queued-build/u;
17
+ const WORKSPACE_SLUG = 'acme';
18
+ const PROJECT_SLUG = 'checkout-api';
11
19
 
12
- function loadedQuery(): WorkflowRunListQuery {
20
+ function loadedQuery(overrides: Partial<WorkflowRunListQuery> = {}): WorkflowRunListQuery {
13
21
  return {
14
22
  isPending: false,
15
23
  isError: false,
@@ -17,6 +25,7 @@ function loadedQuery(): WorkflowRunListQuery {
17
25
  data: {pages: [], pageParams: []},
18
26
  error: null,
19
27
  refetch: () => undefined,
28
+ ...overrides,
20
29
  };
21
30
  }
22
31
 
@@ -26,136 +35,464 @@ describe('WorkflowRunListView', () => {
26
35
  vi.restoreAllMocks();
27
36
  });
28
37
 
29
- test('narrows the list to the selected status filter', async () => {
30
- const user = userEvent.setup();
31
- renderListView([
32
- run('running', 'deploy-web'),
33
- run('failed', 'integration-tests'),
34
- run('succeeded', 'build-image'),
35
- ]);
38
+ describe('filtering', () => {
39
+ test('narrows the list to the selected status', async () => {
40
+ const user = userEvent.setup();
41
+ renderListView([
42
+ run('running', 'deploy-web'),
43
+ run('failed', 'integration-tests'),
44
+ run('succeeded', 'build-image'),
45
+ ]);
36
46
 
37
- await user.click(await screen.findByRole('button', {name: 'Failed'}));
47
+ await selectFilterOption(user, 'Status', 'Failed');
38
48
 
39
- expect(screen.getByText('integration-tests')).toBeInTheDocument();
40
- expect(screen.queryByText('deploy-web')).not.toBeInTheDocument();
41
- expect(screen.queryByText('build-image')).not.toBeInTheDocument();
42
- expect(screen.getByRole('button', {name: 'Failed'})).toHaveAttribute('aria-pressed', 'true');
43
- });
49
+ expect(screen.getByText('integration-tests')).toBeInTheDocument();
50
+ expect(screen.queryByText('deploy-web')).not.toBeInTheDocument();
51
+ expect(screen.queryByText('build-image')).not.toBeInTheDocument();
52
+ });
44
53
 
45
- test('the Running filter keeps in-progress pending runs alongside running ones', async () => {
46
- const user = userEvent.setup();
47
- renderListView([
48
- run('running', 'deploy-web'),
49
- run('pending', 'queued-build'),
50
- run('succeeded', 'build-image'),
51
- ]);
54
+ test('keeps both statuses when two are selected', async () => {
55
+ const user = userEvent.setup();
56
+ renderListView([
57
+ run('running', 'deploy-web'),
58
+ run('failed', 'integration-tests'),
59
+ run('succeeded', 'build-image'),
60
+ ]);
52
61
 
53
- await user.click(await screen.findByRole('button', {name: 'Running'}));
62
+ await selectFilterOption(user, 'Status', 'Failed');
63
+ await selectFilterOption(user, 'Status', 'Succeeded');
54
64
 
55
- expect(screen.getByText('deploy-web')).toBeInTheDocument();
56
- expect(screen.getByText('queued-build')).toBeInTheDocument();
57
- expect(screen.queryByText('build-image')).not.toBeInTheDocument();
58
- });
65
+ expect(screen.getByText('integration-tests')).toBeInTheDocument();
66
+ expect(screen.getByText('build-image')).toBeInTheDocument();
67
+ expect(screen.queryByText('deploy-web')).not.toBeInTheDocument();
68
+ });
59
69
 
60
- test('renders optimistic temp runs without a navigable link', async () => {
61
- renderListView([run('pending', 'queued-build', 'temp-1234'), run('running', 'deploy-web')]);
70
+ test('the running status keeps in-progress pending runs alongside running ones', async () => {
71
+ const user = userEvent.setup();
72
+ renderListView([
73
+ run('running', 'deploy-web'),
74
+ run('pending', 'queued-build'),
75
+ run('succeeded', 'build-image'),
76
+ ]);
62
77
 
63
- // The canonical run is a link to its detail page; the optimistic temp run is shown but
64
- // not yet navigable (its detail page does not exist until the canonical row replaces it).
65
- const links = await screen.findAllByRole('link');
66
- expect(links.some((link) => link.textContent?.includes('deploy-web'))).toBe(true);
67
- expect(links.some((link) => link.textContent?.includes('queued-build'))).toBe(false);
68
- expect(screen.getByText('queued-build')).toBeInTheDocument();
69
- });
78
+ await selectFilterOption(user, 'Status', 'Running');
70
79
 
71
- test('shows a finished run duration in the row metadata', async () => {
72
- renderListView([
73
- run('succeeded', 'build-image', 'run-build-image', {
74
- started_at: '2026-05-07T01:00:00.000Z',
75
- finished_at: '2026-05-07T01:02:14.000Z',
76
- }),
77
- ]);
78
-
79
- const duration = await screen.findByText('2m 14s');
80
- expect(duration).toBeInTheDocument();
81
- expect(duration).toHaveAttribute('aria-label', 'ran 2m 14s');
82
- expect(
83
- screen.getByRole('link', {
84
- name: (name) => name.includes('build-image') && name.includes('ran 2m 14s'),
85
- }),
86
- ).toBeInTheDocument();
87
- });
80
+ expect(screen.getByText('deploy-web')).toBeInTheDocument();
81
+ expect(screen.getByText('queued-build')).toBeInTheDocument();
82
+ expect(screen.queryByText('build-image')).not.toBeInTheDocument();
83
+ });
84
+
85
+ test('narrows the list by branch, using options read off the loaded runs', async () => {
86
+ const user = userEvent.setup();
87
+ renderListView([
88
+ run('succeeded', 'deploy-web', 'run-1', {
89
+ trigger_reference: reference({ref: 'refs/heads/main'}),
90
+ }),
91
+ run('succeeded', 'deploy-api', 'run-2', {
92
+ trigger_reference: reference({ref: 'refs/heads/release/v2'}),
93
+ }),
94
+ ]);
95
+
96
+ await selectFilterOption(user, 'Branch', 'release/v2');
97
+
98
+ expect(screen.getByText('deploy-api')).toBeInTheDocument();
99
+ expect(screen.queryByText('deploy-web')).not.toBeInTheDocument();
100
+ });
101
+
102
+ test('narrows the list by actor', async () => {
103
+ const user = userEvent.setup();
104
+ renderListView([
105
+ run('succeeded', 'deploy-web', 'run-1', {trigger_reference: reference({actor: 'octocat'})}),
106
+ run('succeeded', 'deploy-api', 'run-2', {trigger_reference: reference({actor: 'hubot'})}),
107
+ ]);
108
+
109
+ await selectFilterOption(user, 'Actor', 'hubot');
110
+
111
+ expect(screen.getByText('deploy-api')).toBeInTheDocument();
112
+ expect(screen.queryByText('deploy-web')).not.toBeInTheDocument();
113
+ });
114
+
115
+ test('narrows the list by trigger event', async () => {
116
+ const user = userEvent.setup();
117
+ renderListView([
118
+ run('succeeded', 'deploy-web', 'run-1', {trigger_event: 'push'}),
119
+ run('succeeded', 'deploy-api', 'run-2', {trigger_event: 'pull_request'}),
120
+ ]);
121
+
122
+ await selectFilterOption(user, 'Event', 'pull_request');
123
+
124
+ expect(screen.getByText('deploy-api')).toBeInTheDocument();
125
+ expect(screen.queryByText('deploy-web')).not.toBeInTheDocument();
126
+ });
127
+
128
+ test('names the selected value on the trigger instead of only a count', async () => {
129
+ const user = userEvent.setup();
130
+ renderListView([run('failed', 'integration-tests')]);
131
+
132
+ await selectFilterOption(user, 'Status', 'Failed');
133
+
134
+ const trigger = screen.getByRole('button', {name: filterTrigger('Status')});
135
+ expect(trigger).toHaveTextContent('Status: Failed');
136
+ // The value is in the accessible name too, not just the visible label.
137
+ expect(trigger).toHaveAccessibleName('Status: Failed filter');
138
+ });
139
+
140
+ test('restores every row after the filters are cleared', async () => {
141
+ const user = userEvent.setup();
142
+ renderListView([
143
+ run('running', 'deploy-web'),
144
+ run('failed', 'integration-tests'),
145
+ run('succeeded', 'build-image'),
146
+ ]);
147
+ await user.type(await screen.findByLabelText('Search runs'), 'no-such-run');
148
+
149
+ expect(screen.getByText('No matching runs')).toBeInTheDocument();
150
+
151
+ await user.click(screen.getByRole('button', {name: 'Clear filters'}));
88
152
 
89
- test('shows a live running run duration in the row metadata', async () => {
90
- vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
91
-
92
- renderListView([
93
- run('running', 'deploy-web', 'run-deploy-web', {
94
- started_at: '2026-05-07T01:00:00.000Z',
95
- finished_at: null,
96
- }),
97
- ]);
98
-
99
- const duration = await screen.findByText('2m 14s');
100
- expect(duration).toBeInTheDocument();
101
- expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
102
- expect(
103
- screen.getByRole('link', {
104
- name: (name) => name.includes('deploy-web') && name.includes('running 2m 14s'),
105
- }),
106
- ).toBeInTheDocument();
153
+ expect(screen.getByText('deploy-web')).toBeInTheDocument();
154
+ expect(screen.getByText('integration-tests')).toBeInTheDocument();
155
+ expect(screen.getByText('build-image')).toBeInTheDocument();
156
+ expect(screen.getByLabelText('Search runs')).toHaveValue('');
157
+ });
158
+
159
+ test('offers no way to clear filters while none are active', async () => {
160
+ renderListView([run('succeeded', 'build-image')]);
161
+
162
+ expect(await screen.findByText('build-image')).toBeInTheDocument();
163
+ expect(screen.queryByRole('button', {name: 'Clear filters'})).not.toBeInTheDocument();
164
+ });
107
165
  });
108
166
 
109
- test('scopes the trigger tooltip to the trigger label', async () => {
110
- const user = userEvent.setup();
111
- renderListView([run('failed', 'integration-tests')]);
167
+ describe('interaction states', () => {
168
+ test('renders skeleton rows while the first page loads', async () => {
169
+ renderListView([], {query: loadedQuery({isPending: true, data: undefined})});
170
+
171
+ expect(await screen.findByLabelText('Loading runs')).toBeInTheDocument();
172
+ expect(screen.queryByText('No runs yet')).not.toBeInTheDocument();
173
+ });
174
+
175
+ test('keeps the filter row usable while loading', async () => {
176
+ renderListView([], {query: loadedQuery({isPending: true, data: undefined})});
177
+
178
+ expect(await screen.findByLabelText('Search runs')).toBeInTheDocument();
179
+ });
180
+
181
+ test('offers the onboarding call to action when a project has no runs at all', async () => {
182
+ renderListView([]);
183
+
184
+ expect(await screen.findByText('No runs yet')).toBeInTheDocument();
185
+ expect(screen.getByRole('link', {name: 'View workflows'})).toBeInTheDocument();
186
+ expect(screen.queryByRole('button', {name: 'Show all runs'})).not.toBeInTheDocument();
187
+ });
188
+
189
+ test('offers a way out rather than onboarding when filters emptied the list', async () => {
190
+ const user = userEvent.setup();
191
+ renderListView([run('succeeded', 'build-image')]);
192
+
193
+ await user.type(await screen.findByLabelText('Search runs'), 'no-such-run');
194
+
195
+ expect(screen.getByText('No matching runs')).toBeInTheDocument();
196
+ expect(screen.queryByText('No runs yet')).not.toBeInTheDocument();
197
+ expect(screen.getByRole('button', {name: 'Show all runs'})).toBeInTheDocument();
198
+ });
199
+
200
+ test('reports a filtered empty list as no matches even when nothing loaded', async () => {
201
+ const user = userEvent.setup();
202
+ renderListView([run('succeeded', 'build-image')]);
203
+
204
+ await selectFilterOption(user, 'Status', 'Cancelled');
205
+
206
+ expect(screen.getByText('No matching runs')).toBeInTheDocument();
207
+ expect(screen.queryByText('No runs yet')).not.toBeInTheDocument();
208
+ });
209
+
210
+ test('renders a failed first fetch as an error, never as an empty list', async () => {
211
+ renderListView([], {
212
+ query: loadedQuery({isError: true, data: undefined, error: new Error('boom')}),
213
+ });
214
+
215
+ expect(await screen.findByLabelText('Search runs')).toBeInTheDocument();
216
+ expect(screen.queryByText('No runs yet')).not.toBeInTheDocument();
217
+ expect(screen.queryByText('No matching runs')).not.toBeInTheDocument();
218
+ });
219
+
220
+ test('still reports no matches under a stale-refresh banner', async () => {
221
+ const user = userEvent.setup();
222
+ renderListView([run('succeeded', 'build-image')], {
223
+ query: loadedQuery({isError: true, error: new Error('boom')}),
224
+ });
225
+
226
+ await user.type(await screen.findByLabelText('Search runs'), 'no-such-run');
227
+
228
+ expect(screen.getByText('Could not refresh workflow runs.')).toBeInTheDocument();
229
+ expect(screen.getByText('No matching runs')).toBeInTheDocument();
230
+ });
231
+
232
+ test('keeps loaded rows and the filters behind a retry when a refetch fails', async () => {
233
+ const user = userEvent.setup();
234
+ const refetch = vi.fn();
235
+ renderListView([run('succeeded', 'build-image')], {
236
+ query: loadedQuery({isError: true, error: new Error('boom'), refetch}),
237
+ });
238
+
239
+ expect(await screen.findByText('build-image')).toBeInTheDocument();
240
+ expect(screen.getByText('Could not refresh workflow runs.')).toBeInTheDocument();
112
241
 
113
- await user.hover(
114
- await screen.findByRole('link', {
115
- name: (name) =>
116
- name.includes('integration-tests') && name.includes('github') && name.includes('push'),
117
- }),
118
- );
242
+ await user.click(screen.getByRole('button', {name: 'Retry'}));
119
243
 
120
- expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
244
+ expect(refetch).toHaveBeenCalledOnce();
245
+ });
121
246
 
122
- await user.hover(screen.getByText('push'));
247
+ test('offers an explicit load more when more pages exist', async () => {
248
+ const user = userEvent.setup();
249
+ const onLoadMore = vi.fn();
250
+ renderListView([run('succeeded', 'recent-run')], {hasNextPage: true, onLoadMore});
123
251
 
124
- expect(await screen.findByRole('tooltip')).toHaveTextContent('github_acme · push');
252
+ await user.click(await screen.findByRole('button', {name: 'Load more runs'}));
253
+
254
+ expect(onLoadMore).toHaveBeenCalledOnce();
255
+ });
256
+
257
+ test('offers older pages before reporting that a filter has no matches', async () => {
258
+ const user = userEvent.setup();
259
+ const onLoadMore = vi.fn();
260
+ renderListView([run('succeeded', 'recent-run')], {hasNextPage: true, onLoadMore});
261
+
262
+ await user.type(await screen.findByLabelText('Search runs'), 'older-run');
263
+
264
+ expect(screen.getByText('No matches in loaded history')).toBeInTheDocument();
265
+ await user.click(screen.getByRole('button', {name: 'Load more runs'}));
266
+ expect(onLoadMore).toHaveBeenCalledOnce();
267
+ });
125
268
  });
126
269
 
127
- test('restores every row after the filters are cleared', async () => {
128
- const user = userEvent.setup();
129
- renderListView([
130
- run('running', 'deploy-web'),
131
- run('failed', 'integration-tests'),
132
- run('succeeded', 'build-image'),
133
- ]);
134
- await user.type(await screen.findByLabelText('Search runs'), 'no-such-run');
270
+ describe('row anatomy', () => {
271
+ test('renders the branch, commit, and actor a source-control trigger carries', async () => {
272
+ renderListView([
273
+ run('succeeded', 'deploy-web', 'run-1', {
274
+ trigger_reference: {
275
+ repository: 'acme/api',
276
+ ref: 'refs/heads/release/v2',
277
+ commit: 'abcdef1234567890',
278
+ actor: 'octocat',
279
+ },
280
+ }),
281
+ ]);
282
+
283
+ expect(await screen.findByText('release/v2')).toBeInTheDocument();
284
+ expect(screen.getByText('abcdef1')).toBeInTheDocument();
285
+ expect(screen.getByText('octocat')).toBeInTheDocument();
286
+ });
287
+
288
+ test('shows a pull request ref by number rather than as a raw ref', async () => {
289
+ renderListView([
290
+ run('succeeded', 'deploy-web', 'run-1', {
291
+ trigger_reference: reference({ref: 'refs/pull/42/head'}),
292
+ }),
293
+ ]);
294
+
295
+ expect(await screen.findByText('#42')).toBeInTheDocument();
296
+ });
297
+
298
+ test('summarizes the job strip as one accessible name rather than one per glyph', async () => {
299
+ renderListView([
300
+ run('failed', 'deploy-web', 'run-1', {
301
+ ...workflowRunJobsFixture(['succeeded', 'failed', 'pending']),
302
+ }),
303
+ ]);
304
+
305
+ expect(
306
+ await screen.findByRole('img', {name: '3 jobs: 1 failed, 1 pending, 1 succeeded'}),
307
+ ).toBeInTheDocument();
308
+ });
309
+
310
+ // The link's aria-label replaces its contents, so the strip's own label is not spoken
311
+ // when the row is announced as a link. Where a run failed has to survive that.
312
+ test('carries the job breakdown into the row link name', async () => {
313
+ renderListView([
314
+ run('failed', 'deploy-web', 'run-1', {
315
+ ...workflowRunJobsFixture(['succeeded', 'failed', 'pending']),
316
+ }),
317
+ ]);
318
+
319
+ expect(
320
+ await screen.findByRole('link', {
321
+ name: (name) => name.includes('3 jobs: 1 failed, 1 pending, 1 succeeded'),
322
+ }),
323
+ ).toBeInTheDocument();
324
+ });
325
+
326
+ test('leaves the job breakdown out of the link name when no jobs are planned', async () => {
327
+ renderListView([run('pending', 'queued-build', 'run-1', {...workflowRunJobsFixture([])})]);
328
+
329
+ const link = await screen.findByRole('link', {name: JOB_SUMMARY_NAME_RE});
330
+ expect(link.getAttribute('aria-label')).not.toContain('jobs:');
331
+ });
332
+
333
+ test('counts the jobs it had to hide beyond the strip threshold', async () => {
334
+ renderListView([run('running', 'deploy-web', 'run-1', {...workflowRunJobsOfStatus(40)})]);
335
+
336
+ expect(await screen.findByText('+33')).toBeInTheDocument();
337
+ expect(screen.getByRole('img', {name: '40 jobs: 40 succeeded'})).toBeInTheDocument();
338
+ });
339
+
340
+ // The API sends a bounded preview, so a run whose only failure sits past it has no failed
341
+ // job in the payload at all. The counts still carry it, and the overflow glyph has to say
342
+ // so rather than showing an unbroken row of green.
343
+ test('reports a failure that sits beyond the fetched preview', async () => {
344
+ const statuses = [
345
+ ...(Array.from({length: 20}, () => 'succeeded') as JobStatusDto[]),
346
+ 'failed' as const,
347
+ ];
348
+ renderListView([run('failed', 'deploy-web', 'run-1', {...workflowRunJobsFixture(statuses)})]);
349
+
350
+ const strip = await screen.findByRole('img', {
351
+ name: '21 jobs: 1 failed, 20 succeeded',
352
+ });
353
+ expect(strip).toBeInTheDocument();
354
+ expect(screen.getByText('+14')).toBeInTheDocument();
355
+ expect(within(strip).getByLabelText('Failed')).toBeInTheDocument();
356
+ });
357
+
358
+ // Nothing caps a workflow's job count, so an exact overflow count is unbounded in width
359
+ // and would eventually paint over the duration column beside it.
360
+ test('abbreviates an overflow count too wide to print exactly', async () => {
361
+ renderListView([
362
+ run('running', 'huge', 'run-1', {
363
+ ...workflowRunJobsOfStatus(12_000, 'pending'),
364
+ }),
365
+ ]);
366
+
367
+ expect(await screen.findByText('+12K')).toBeInTheDocument();
368
+ // The exact figure survives where a precise number is actually read.
369
+ expect(screen.getByRole('img', {name: '12000 jobs: 12000 pending'})).toBeInTheDocument();
370
+ });
371
+
372
+ test('renders no strip for a run whose jobs are not planned yet', async () => {
373
+ renderListView([run('pending', 'queued-build', 'run-1', {...workflowRunJobsFixture([])})]);
135
374
 
136
- expect(screen.getByText('No matching runs')).toBeInTheDocument();
375
+ expect(await screen.findByText('queued-build')).toBeInTheDocument();
376
+ expect(screen.queryByRole('img', {name: JOB_STRIP_LABEL_RE})).not.toBeInTheDocument();
377
+ });
137
378
 
138
- await user.click(await screen.findByRole('button', {name: 'Clear filters'}));
379
+ test('renders optimistic temp runs without a navigable link', async () => {
380
+ const optimisticRun = {
381
+ ...run('pending', 'queued-build', 'temp-1234', {workflow_name: 'CI'}),
382
+ number: null,
383
+ };
384
+ renderListView([optimisticRun, run('running', 'deploy-web')]);
139
385
 
140
- expect(screen.getByText('deploy-web')).toBeInTheDocument();
141
- expect(screen.getByText('integration-tests')).toBeInTheDocument();
142
- expect(screen.getByText('build-image')).toBeInTheDocument();
143
- expect(screen.getByLabelText('Search runs')).toHaveValue('');
386
+ // The canonical run is a link to its detail page; the optimistic temp run is shown but
387
+ // not yet navigable (its detail page does not exist until the canonical row replaces it).
388
+ const links = await screen.findAllByRole('link');
389
+ expect(links.some((link) => link.textContent?.includes('deploy-web'))).toBe(true);
390
+ expect(links.some((link) => link.textContent?.includes('queued-build'))).toBe(false);
391
+ expect(screen.getByText('queued-build')).toBeInTheDocument();
392
+ expect(screen.queryByText('CI #1')).not.toBeInTheDocument();
393
+ });
394
+
395
+ test('shows a finished run duration in the row metadata', async () => {
396
+ renderListView([
397
+ run('succeeded', 'build-image', 'run-build-image', {
398
+ started_at: '2026-05-07T01:00:00.000Z',
399
+ finished_at: '2026-05-07T01:02:14.000Z',
400
+ }),
401
+ ]);
402
+
403
+ const duration = await screen.findByText('2m 14s');
404
+ expect(duration).toHaveAttribute('aria-label', 'ran 2m 14s');
405
+ expect(
406
+ screen.getByRole('link', {
407
+ name: (name) => name.includes('build-image') && name.includes('ran 2m 14s'),
408
+ }),
409
+ ).toBeInTheDocument();
410
+ });
411
+
412
+ test('shows a live running run duration in the row metadata', async () => {
413
+ vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
414
+
415
+ renderListView([
416
+ run('running', 'deploy-web', 'run-deploy-web', {
417
+ started_at: '2026-05-07T01:00:00.000Z',
418
+ finished_at: null,
419
+ }),
420
+ ]);
421
+
422
+ const duration = await screen.findByText('2m 14s');
423
+ expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
424
+ });
425
+
426
+ test('renders the workflow name beside the run number', async () => {
427
+ renderListView([
428
+ run('succeeded', 'Deploy production', 'run-deploy-production', {
429
+ number: 5184,
430
+ workflow_name: 'CI',
431
+ }),
432
+ ]);
433
+
434
+ expect(await screen.findByText('CI #5184')).toBeInTheDocument();
435
+ expect(
436
+ screen.getByRole('link', {name: (name) => name.includes('CI #5184')}),
437
+ ).toBeInTheDocument();
438
+ });
439
+
440
+ test('scopes the trigger tooltip to the trigger label', async () => {
441
+ const user = userEvent.setup();
442
+ renderListView([run('failed', 'integration-tests')]);
443
+
444
+ await user.hover(await screen.findByText('push'));
445
+
446
+ expect(await screen.findByRole('tooltip')).toHaveTextContent('github_acme · push');
447
+ });
144
448
  });
145
449
  });
146
450
 
147
- function renderListView(runs: WorkflowRunListItem[]) {
451
+ function renderListView(
452
+ runs: WorkflowRunListItem[],
453
+ {
454
+ query = loadedQuery(),
455
+ ...options
456
+ }: Partial<Pick<WorkflowRunListViewProps, 'hasNextPage' | 'onLoadMore' | 'query'>> = {},
457
+ ) {
148
458
  // Row links need router context; the query and data stay injected by props.
149
459
  renderWithRouter(
150
460
  <WorkflowRunListView
151
461
  runs={runs}
152
- query={loadedQuery()}
153
- workspaceId={WORKSPACE_ID}
154
- projectId={PROJECT_ID}
462
+ query={query}
463
+ workspaceSlug={WORKSPACE_SLUG}
464
+ projectSlug={PROJECT_SLUG}
465
+ {...options}
155
466
  />,
156
467
  );
157
468
  }
158
469
 
470
+ /** Matches a filter trigger by its label, whatever value it currently reports. */
471
+ function filterTrigger(label: string): RegExp {
472
+ return new RegExp(`^${label}\\b.*filter$`, 'u');
473
+ }
474
+
475
+ async function selectFilterOption(
476
+ user: ReturnType<typeof userEvent.setup>,
477
+ label: string,
478
+ option: string,
479
+ ) {
480
+ await user.click(await screen.findByRole('button', {name: filterTrigger(label)}));
481
+ const menu = await screen.findByRole('menu');
482
+ await user.click(within(menu).getByRole('menuitemcheckbox', {name: option}));
483
+ await user.keyboard('{Escape}');
484
+ }
485
+
486
+ function reference(overrides: Partial<NonNullable<WorkflowRunListItem['triggerReference']>> = {}) {
487
+ return {
488
+ repository: 'acme/api',
489
+ ref: 'refs/heads/main',
490
+ commit: 'abcdef1234567890',
491
+ actor: 'octocat',
492
+ ...overrides,
493
+ };
494
+ }
495
+
159
496
  function run(
160
497
  status: WorkflowRunStatus,
161
498
  name: string,