@flanksource/clicky-ui 0.3.47 → 0.3.48

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 (169) hide show
  1. package/README.md +49 -0
  2. package/dist/comments/CommentSidePanel.cjs +10 -4
  3. package/dist/comments/CommentSidePanel.cjs.map +1 -1
  4. package/dist/comments/CommentSidePanel.d.ts +2 -0
  5. package/dist/comments/CommentSidePanel.d.ts.map +1 -1
  6. package/dist/comments/CommentSidePanel.js +10 -4
  7. package/dist/comments/CommentSidePanel.js.map +1 -1
  8. package/dist/comments/CommentThread.cjs +2 -0
  9. package/dist/comments/CommentThread.cjs.map +1 -1
  10. package/dist/comments/CommentThread.d.ts +2 -1
  11. package/dist/comments/CommentThread.d.ts.map +1 -1
  12. package/dist/comments/CommentThread.js +2 -0
  13. package/dist/comments/CommentThread.js.map +1 -1
  14. package/dist/comments/CommentThreadList.cjs +3 -0
  15. package/dist/comments/CommentThreadList.cjs.map +1 -1
  16. package/dist/comments/CommentThreadList.d.ts +3 -1
  17. package/dist/comments/CommentThreadList.d.ts.map +1 -1
  18. package/dist/comments/CommentThreadList.js +3 -0
  19. package/dist/comments/CommentThreadList.js.map +1 -1
  20. package/dist/data/CodeLine.cjs +4 -2
  21. package/dist/data/CodeLine.cjs.map +1 -1
  22. package/dist/data/CodeLine.d.ts.map +1 -1
  23. package/dist/data/CodeLine.js +4 -2
  24. package/dist/data/CodeLine.js.map +1 -1
  25. package/dist/data/SymbolBadge.cjs +61 -0
  26. package/dist/data/SymbolBadge.cjs.map +1 -0
  27. package/dist/data/SymbolBadge.d.ts +21 -0
  28. package/dist/data/SymbolBadge.d.ts.map +1 -0
  29. package/dist/data/SymbolBadge.js +61 -0
  30. package/dist/data/SymbolBadge.js.map +1 -0
  31. package/dist/data/test-runner/TestDetailPanel.cjs +26 -12
  32. package/dist/data/test-runner/TestDetailPanel.cjs.map +1 -1
  33. package/dist/data/test-runner/TestDetailPanel.d.ts.map +1 -1
  34. package/dist/data/test-runner/TestDetailPanel.js +27 -13
  35. package/dist/data/test-runner/TestDetailPanel.js.map +1 -1
  36. package/dist/data/test-runner/TestRunner.cjs +6 -0
  37. package/dist/data/test-runner/TestRunner.cjs.map +1 -1
  38. package/dist/data/test-runner/TestRunner.d.ts +3 -1
  39. package/dist/data/test-runner/TestRunner.d.ts.map +1 -1
  40. package/dist/data/test-runner/TestRunner.js +6 -0
  41. package/dist/data/test-runner/TestRunner.js.map +1 -1
  42. package/dist/data/test-runner/TestTreeNode.cjs +2 -1
  43. package/dist/data/test-runner/TestTreeNode.cjs.map +1 -1
  44. package/dist/data/test-runner/TestTreeNode.d.ts.map +1 -1
  45. package/dist/data/test-runner/TestTreeNode.js +2 -1
  46. package/dist/data/test-runner/TestTreeNode.js.map +1 -1
  47. package/dist/data/test-runner/adapter.cjs +4 -0
  48. package/dist/data/test-runner/adapter.cjs.map +1 -1
  49. package/dist/data/test-runner/adapter.d.ts +9 -0
  50. package/dist/data/test-runner/adapter.d.ts.map +1 -1
  51. package/dist/data/test-runner/adapter.js +4 -0
  52. package/dist/data/test-runner/adapter.js.map +1 -1
  53. package/dist/data/test-runner/context.cjs.map +1 -1
  54. package/dist/data/test-runner/context.d.ts +2 -0
  55. package/dist/data/test-runner/context.d.ts.map +1 -1
  56. package/dist/data/test-runner/context.js.map +1 -1
  57. package/dist/data/test-runner/index.d.ts +2 -2
  58. package/dist/data/test-runner/index.d.ts.map +1 -1
  59. package/dist/data/test-runner/marks.cjs +42 -1
  60. package/dist/data/test-runner/marks.cjs.map +1 -1
  61. package/dist/data/test-runner/marks.d.ts +10 -0
  62. package/dist/data/test-runner/marks.d.ts.map +1 -1
  63. package/dist/data/test-runner/marks.js +42 -1
  64. package/dist/data/test-runner/marks.js.map +1 -1
  65. package/dist/data/traces/TerminalCastPlayer.d.ts +21 -0
  66. package/dist/data/traces/TerminalCastPlayer.d.ts.map +1 -0
  67. package/dist/data/traces/TraceHarView.d.ts +13 -0
  68. package/dist/data/traces/TraceHarView.d.ts.map +1 -0
  69. package/dist/data/traces/cast.d.ts +9 -0
  70. package/dist/data/traces/cast.d.ts.map +1 -0
  71. package/dist/data/traces/fixtures.d.ts +8 -0
  72. package/dist/data/traces/fixtures.d.ts.map +1 -0
  73. package/dist/data/traces/har-adapter.d.ts +13 -0
  74. package/dist/data/traces/har-adapter.d.ts.map +1 -0
  75. package/dist/data/traces/terminal-screen.d.ts +45 -0
  76. package/dist/data/traces/terminal-screen.d.ts.map +1 -0
  77. package/dist/data/traces/terminal-timeline.d.ts +26 -0
  78. package/dist/data/traces/terminal-timeline.d.ts.map +1 -0
  79. package/dist/data/traces/types.d.ts +141 -0
  80. package/dist/data/traces/types.d.ts.map +1 -0
  81. package/dist/data/verification/VerificationResults.cjs +4 -0
  82. package/dist/data/verification/VerificationResults.cjs.map +1 -1
  83. package/dist/data/verification/VerificationResults.d.ts +3 -1
  84. package/dist/data/verification/VerificationResults.d.ts.map +1 -1
  85. package/dist/data/verification/VerificationResults.js +4 -0
  86. package/dist/data/verification/VerificationResults.js.map +1 -1
  87. package/dist/data/verification/adapterViews.cjs +12 -3
  88. package/dist/data/verification/adapterViews.cjs.map +1 -1
  89. package/dist/data/verification/adapterViews.d.ts +4 -1
  90. package/dist/data/verification/adapterViews.d.ts.map +1 -1
  91. package/dist/data/verification/adapterViews.js +12 -3
  92. package/dist/data/verification/adapterViews.js.map +1 -1
  93. package/dist/data/verification/adapters.cjs +53 -10
  94. package/dist/data/verification/adapters.cjs.map +1 -1
  95. package/dist/data/verification/adapters.d.ts +7 -1
  96. package/dist/data/verification/adapters.d.ts.map +1 -1
  97. package/dist/data/verification/adapters.js +53 -10
  98. package/dist/data/verification/adapters.js.map +1 -1
  99. package/dist/data/verification/index.d.ts +4 -2
  100. package/dist/data/verification/index.d.ts.map +1 -1
  101. package/dist/data/verification/stepKind.cjs +217 -0
  102. package/dist/data/verification/stepKind.cjs.map +1 -0
  103. package/dist/data/verification/stepKind.d.ts +87 -0
  104. package/dist/data/verification/stepKind.d.ts.map +1 -0
  105. package/dist/data/verification/stepKind.js +217 -0
  106. package/dist/data/verification/stepKind.js.map +1 -0
  107. package/dist/data/verification/stepKindViews.cjs +111 -0
  108. package/dist/data/verification/stepKindViews.cjs.map +1 -0
  109. package/dist/data/verification/stepKindViews.d.ts +18 -0
  110. package/dist/data/verification/stepKindViews.d.ts.map +1 -0
  111. package/dist/data/verification/stepKindViews.js +111 -0
  112. package/dist/data/verification/stepKindViews.js.map +1 -0
  113. package/dist/data/verification/verifyReportTests.cjs +1 -0
  114. package/dist/data/verification/verifyReportTests.cjs.map +1 -1
  115. package/dist/data/verification/verifyReportTests.d.ts +2 -1
  116. package/dist/data/verification/verifyReportTests.d.ts.map +1 -1
  117. package/dist/data/verification/verifyReportTests.js +1 -0
  118. package/dist/data/verification/verifyReportTests.js.map +1 -1
  119. package/dist/data/version-info.cjs +3 -3
  120. package/dist/data/version-info.js +3 -3
  121. package/dist/data.cjs +14 -0
  122. package/dist/data.cjs.map +1 -1
  123. package/dist/data.d.ts +1 -0
  124. package/dist/data.d.ts.map +1 -1
  125. package/dist/data.js +16 -2
  126. package/dist/data.js.map +1 -1
  127. package/dist/index.cjs +16 -0
  128. package/dist/index.cjs.map +1 -1
  129. package/dist/index.js +18 -2
  130. package/dist/index.js.map +1 -1
  131. package/dist/rpc/FilterForm.cjs +9 -0
  132. package/dist/rpc/FilterForm.cjs.map +1 -1
  133. package/dist/rpc/FilterForm.d.ts +5 -1
  134. package/dist/rpc/FilterForm.d.ts.map +1 -1
  135. package/dist/rpc/FilterForm.js +9 -0
  136. package/dist/rpc/FilterForm.js.map +1 -1
  137. package/dist/rpc/OperationArgsForm.cjs +153 -0
  138. package/dist/rpc/OperationArgsForm.cjs.map +1 -0
  139. package/dist/rpc/OperationArgsForm.d.ts +16 -0
  140. package/dist/rpc/OperationArgsForm.d.ts.map +1 -0
  141. package/dist/rpc/OperationArgsForm.js +153 -0
  142. package/dist/rpc/OperationArgsForm.js.map +1 -0
  143. package/dist/rpc/OperationScheduleDialog.cjs +61 -17
  144. package/dist/rpc/OperationScheduleDialog.cjs.map +1 -1
  145. package/dist/rpc/OperationScheduleDialog.d.ts +3 -0
  146. package/dist/rpc/OperationScheduleDialog.d.ts.map +1 -1
  147. package/dist/rpc/OperationScheduleDialog.js +62 -18
  148. package/dist/rpc/OperationScheduleDialog.js.map +1 -1
  149. package/dist/rpc/OperationSchedules.cjs +6 -1
  150. package/dist/rpc/OperationSchedules.cjs.map +1 -1
  151. package/dist/rpc/OperationSchedules.d.ts.map +1 -1
  152. package/dist/rpc/OperationSchedules.js +6 -1
  153. package/dist/rpc/OperationSchedules.js.map +1 -1
  154. package/dist/rpc/operation-schedule-model.cjs +0 -7
  155. package/dist/rpc/operation-schedule-model.cjs.map +1 -1
  156. package/dist/rpc/operation-schedule-model.d.ts +0 -2
  157. package/dist/rpc/operation-schedule-model.d.ts.map +1 -1
  158. package/dist/rpc/operation-schedule-model.js +0 -7
  159. package/dist/rpc/operation-schedule-model.js.map +1 -1
  160. package/dist/rpc/operation-schedule-types.d.ts +6 -1
  161. package/dist/rpc/operation-schedule-types.d.ts.map +1 -1
  162. package/dist/rpc.cjs +2 -0
  163. package/dist/rpc.cjs.map +1 -1
  164. package/dist/rpc.d.ts +1 -0
  165. package/dist/rpc.d.ts.map +1 -1
  166. package/dist/rpc.js +2 -0
  167. package/dist/rpc.js.map +1 -1
  168. package/dist/styles.css +1 -1
  169. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TestDetailPanel.cjs","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actions }: { node: Test; actions?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && node.framework !== \"task\";\n const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={node.framework} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(node)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(node)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const node = runner.selected;\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!node) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(node, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["sum","jsxs","jsx","TestFailureDetail","TestOutput","TestAttempts","JsonView","copy","UiCopy","UiCheck","UiWarningCircle","useTestRunner","useCopyFlash","useMemo","UiJson","buildNodeJson","SplitButton","buildNodeBrief","outcomeFor","OutcomeIcon","Icon","UiClass","ToolIcon","UiClock","formatTestDuration","Button","UiRefresh","UiStop","useState","_a","cn","TabButton"],"mappings":"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"}
1
+ {"version":3,"file":"TestDetailPanel.cjs","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { Select } from \"../../components/select\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { toolKeyFor } from \"./marks\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actionNode, actions, attemptChooser }: { node: Test; actionNode: Test; actions?: ReactNode; attemptChooser?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && actionNode.framework !== \"task\";\n const canStop = !!runner.onStop && !!actionNode.can_stop && !!actionNode.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={toolKeyFor(node)} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {attemptChooser}\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(actionNode)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(actionNode)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const selected = runner.selected;\n const [attemptSelection, setAttemptSelection] = useState<{ key: string; id: number } | null>(null);\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!selected) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const attempts = runner.adapters.resolveAttempts(selected, runner);\n const key = selected.task_id ?? selected.name;\n const attemptId = runner.activeAttempt ?? (attemptSelection?.key === key ? attemptSelection.id : undefined);\n const attempt = attempts.find((a) => a.id === attemptId) ?? attempts.at(-1);\n const node = attempts.length > 1 && attempt ? attempt.node : selected;\n const attemptChooser = attempts.length > 1 && attempt ? (\n <div className=\"w-44 shrink-0\">\n <Select aria-label=\"Attempt\" value={String(attempt.id)} className=\"h-7 text-xs\" onChange={(event) => {\n const id = Number(event.target.value);\n if (runner.onAttemptChange) runner.onAttemptChange(id);\n else setAttemptSelection({ key, id });\n }} options={attempts.map((a) => ({ value: String(a.id), label: `Attempt ${a.id}/${attempts.length} · ${a.status}${a.durationMs !== undefined ? ` · ${formatTestDuration(a.durationMs * 1e6)}` : \"\"}` }))} />\n </div>\n ) : null;\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(selected, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node: selected, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["sum","jsxs","jsx","TestFailureDetail","TestOutput","TestAttempts","JsonView","copy","UiCopy","UiCheck","UiWarningCircle","useTestRunner","useCopyFlash","useMemo","UiJson","buildNodeJson","SplitButton","buildNodeBrief","outcomeFor","OutcomeIcon","Icon","UiClass","ToolIcon","toolKeyFor","UiClock","formatTestDuration","Button","UiRefresh","UiStop","useState","_a","Select","cn","TabButton"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TestDetailPanel.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"names":[],"mappings":"AAqKA;;;;GAIG;AACH,wBAAgB,eAAe,gCAoE9B"}
1
+ {"version":3,"file":"TestDetailPanel.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"names":[],"mappings":"AAwKA;;;;GAIG;AACH,wBAAgB,eAAe,gCAoF9B"}
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState, useMemo } from "react";
3
3
  import { Icon } from "../Icon.js";
4
4
  import { Button } from "../../components/button.js";
5
+ import { Select } from "../../components/select.js";
5
6
  import { SplitButton } from "../../components/SplitButton.js";
6
7
  import { useCopyFlash } from "../../components/clipboard.js";
7
8
  import { cn } from "../../lib/utils.js";
@@ -9,7 +10,8 @@ import { JsonView } from "../JsonView.js";
9
10
  import { TabButton } from "../TabButton.js";
10
11
  import { useTestRunner } from "./context.js";
11
12
  import { buildNodeJson, buildNodeBrief } from "./copy.js";
12
- import { sum, outcomeFor, formatTestDuration } from "./status.js";
13
+ import { formatTestDuration, sum, outcomeFor } from "./status.js";
14
+ import { toolKeyFor } from "./marks.js";
13
15
  import { OutcomeIcon, ToolIcon } from "./MarkIcons.js";
14
16
  import { TestFailureDetail, TestOutput, TestAttempts } from "./TestFailureDetail.js";
15
17
  import { UiClass } from "../../icons/components/UiClass.js";
@@ -85,11 +87,11 @@ function CopyDetailsButton({ node }) {
85
87
  }
86
88
  );
87
89
  }
88
- function DetailHeader({ node, actions }) {
90
+ function DetailHeader({ node, actionNode, actions, attemptChooser }) {
89
91
  const runner = useTestRunner();
90
92
  const outcome = outcomeFor(node);
91
- const canRerun = !!runner.onRerun && node.framework !== "task";
92
- const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;
93
+ const canRerun = !!runner.onRerun && actionNode.framework !== "task";
94
+ const canStop = !!runner.onStop && !!actionNode.can_stop && !!actionNode.task_id;
93
95
  const location = node.file ? `${node.file}${node.line ? `:${node.line}` : ""}` : void 0;
94
96
  return /* @__PURE__ */ jsxs(
95
97
  "div",
@@ -100,7 +102,7 @@ function DetailHeader({ node, actions }) {
100
102
  outcome ? /* @__PURE__ */ jsx(OutcomeIcon, { outcome, className: "text-lg" }) : /* @__PURE__ */ jsx(Icon, { icon: UiClass, className: "shrink-0 text-lg text-muted-foreground" }),
101
103
  /* @__PURE__ */ jsx("h2", { className: "min-w-0 shrink truncate text-base font-semibold", title: node.name, children: node.name }),
102
104
  node.framework && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground", children: [
103
- /* @__PURE__ */ jsx(ToolIcon, { tool: node.framework, className: "text-sm" }),
105
+ /* @__PURE__ */ jsx(ToolIcon, { tool: toolKeyFor(node), className: "text-sm" }),
104
106
  node.framework
105
107
  ] }),
106
108
  node.duration ? /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground", children: [
@@ -116,6 +118,7 @@ function DetailHeader({ node, actions }) {
116
118
  }
117
119
  ),
118
120
  /* @__PURE__ */ jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [
121
+ attemptChooser,
119
122
  actions,
120
123
  /* @__PURE__ */ jsx(CopyDetailsButton, { node }),
121
124
  canRerun && /* @__PURE__ */ jsxs(
@@ -123,7 +126,7 @@ function DetailHeader({ node, actions }) {
123
126
  {
124
127
  size: "sm",
125
128
  disabled: runner.busy.rerun,
126
- onClick: () => runner.onRerun(node),
129
+ onClick: () => runner.onRerun(actionNode),
127
130
  title: "Rerun this test or subtree",
128
131
  children: [
129
132
  /* @__PURE__ */ jsx(Icon, { icon: UiRefresh, className: "mr-1 text-sm" }),
@@ -137,7 +140,7 @@ function DetailHeader({ node, actions }) {
137
140
  size: "sm",
138
141
  variant: "destructive",
139
142
  disabled: runner.busy.stop,
140
- onClick: () => runner.onStop(node),
143
+ onClick: () => runner.onStop(actionNode),
141
144
  title: "Stop this running task",
142
145
  children: [
143
146
  /* @__PURE__ */ jsx(Icon, { icon: UiStop, className: "mr-1 text-sm" }),
@@ -153,7 +156,8 @@ function DetailHeader({ node, actions }) {
153
156
  function TestDetailPanel() {
154
157
  var _a;
155
158
  const runner = useTestRunner();
156
- const node = runner.selected;
159
+ const selected = runner.selected;
160
+ const [attemptSelection, setAttemptSelection] = useState(null);
157
161
  const controlled = runner.activeTab !== void 0;
158
162
  const [internalTab, setInternalTab] = useState("detail");
159
163
  const activeTab = controlled ? runner.activeTab : internalTab;
@@ -162,22 +166,32 @@ function TestDetailPanel() {
162
166
  if (controlled) (_a2 = runner.onTabChange) == null ? void 0 : _a2.call(runner, id);
163
167
  else setInternalTab(id);
164
168
  };
165
- if (!node) {
169
+ if (!selected) {
166
170
  return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
167
171
  /* @__PURE__ */ jsx(Icon, { icon: UiClass, className: "mb-2 block text-4xl" }),
168
172
  "Select a test to view its details."
169
173
  ] }) });
170
174
  }
175
+ const attempts = runner.adapters.resolveAttempts(selected, runner);
176
+ const key = selected.task_id ?? selected.name;
177
+ const attemptId = runner.activeAttempt ?? ((attemptSelection == null ? void 0 : attemptSelection.key) === key ? attemptSelection.id : void 0);
178
+ const attempt = attempts.find((a) => a.id === attemptId) ?? attempts.at(-1);
179
+ const node = attempts.length > 1 && attempt ? attempt.node : selected;
180
+ const attemptChooser = attempts.length > 1 && attempt ? /* @__PURE__ */ jsx("div", { className: "w-44 shrink-0", children: /* @__PURE__ */ jsx(Select, { "aria-label": "Attempt", value: String(attempt.id), className: "h-7 text-xs", onChange: (event) => {
181
+ const id = Number(event.target.value);
182
+ if (runner.onAttemptChange) runner.onAttemptChange(id);
183
+ else setAttemptSelection({ key, id });
184
+ }, options: attempts.map((a) => ({ value: String(a.id), label: `Attempt ${a.id}/${attempts.length} · ${a.status}${a.durationMs !== void 0 ? ` · ${formatTestDuration(a.durationMs * 1e6)}` : ""}` })) }) }) : null;
171
185
  const detailAdapter = runner.adapters.resolveDetail(node, runner);
172
- const actionAdapter = runner.adapters.resolveActions(node, runner);
186
+ const actionAdapter = runner.adapters.resolveActions(selected, runner);
173
187
  const tabs = runner.adapters.resolveTabs(node, runner);
174
188
  const ownsScroll = runner.adapters.ownsScroll(node, runner);
175
189
  const body = (detailAdapter == null ? void 0 : detailAdapter.renderDetail) ? detailAdapter.renderDetail({ node, runner }) : /* @__PURE__ */ jsx(DefaultDetailBody, { node });
176
- const actions = (_a = actionAdapter == null ? void 0 : actionAdapter.nodeActions) == null ? void 0 : _a.call(actionAdapter, { node, runner });
190
+ const actions = (_a = actionAdapter == null ? void 0 : actionAdapter.nodeActions) == null ? void 0 : _a.call(actionAdapter, { node: selected, runner });
177
191
  const detailRegion = cn("min-h-0 flex-1 [container-type:size]", !ownsScroll && "overflow-y-auto");
178
192
  if (tabs.length === 0) {
179
193
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
180
- /* @__PURE__ */ jsx(DetailHeader, { node, actions }),
194
+ /* @__PURE__ */ jsx(DetailHeader, { node, actionNode: selected, actions, attemptChooser }),
181
195
  /* @__PURE__ */ jsx("div", { className: detailRegion, children: body })
182
196
  ] });
183
197
  }
@@ -185,7 +199,7 @@ function TestDetailPanel() {
185
199
  const allTabs = [detailTab, ...tabs];
186
200
  const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;
187
201
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
188
- /* @__PURE__ */ jsx(DetailHeader, { node, actions }),
202
+ /* @__PURE__ */ jsx(DetailHeader, { node, actionNode: selected, actions, attemptChooser }),
189
203
  /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1", children: allTabs.map((t) => /* @__PURE__ */ jsx(
190
204
  TabButton,
191
205
  {
@@ -1 +1 @@
1
- {"version":3,"file":"TestDetailPanel.js","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actions }: { node: Test; actions?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && node.framework !== \"task\";\n const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={node.framework} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(node)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(node)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const node = runner.selected;\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!node) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(node, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"TestDetailPanel.js","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { Select } from \"../../components/select\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { toolKeyFor } from \"./marks\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actionNode, actions, attemptChooser }: { node: Test; actionNode: Test; actions?: ReactNode; attemptChooser?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && actionNode.framework !== \"task\";\n const canStop = !!runner.onStop && !!actionNode.can_stop && !!actionNode.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={toolKeyFor(node)} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {attemptChooser}\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(actionNode)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(actionNode)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const selected = runner.selected;\n const [attemptSelection, setAttemptSelection] = useState<{ key: string; id: number } | null>(null);\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!selected) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const attempts = runner.adapters.resolveAttempts(selected, runner);\n const key = selected.task_id ?? selected.name;\n const attemptId = runner.activeAttempt ?? (attemptSelection?.key === key ? attemptSelection.id : undefined);\n const attempt = attempts.find((a) => a.id === attemptId) ?? attempts.at(-1);\n const node = attempts.length > 1 && attempt ? attempt.node : selected;\n const attemptChooser = attempts.length > 1 && attempt ? (\n <div className=\"w-44 shrink-0\">\n <Select aria-label=\"Attempt\" value={String(attempt.id)} className=\"h-7 text-xs\" onChange={(event) => {\n const id = Number(event.target.value);\n if (runner.onAttemptChange) runner.onAttemptChange(id);\n else setAttemptSelection({ key, id });\n }} options={attempts.map((a) => ({ value: String(a.id), label: `Attempt ${a.id}/${attempts.length} · ${a.status}${a.durationMs !== undefined ? ` · ${formatTestDuration(a.durationMs * 1e6)}` : \"\"}` }))} />\n </div>\n ) : null;\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(selected, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node: selected, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
@@ -34,6 +34,8 @@ function TestRunner({
34
34
  busy,
35
35
  activeTab,
36
36
  onTabChange,
37
+ activeAttempt,
38
+ onAttemptChange,
37
39
  onSelect,
38
40
  onFiltersChange,
39
41
  onExpandAllChange,
@@ -63,6 +65,8 @@ function TestRunner({
63
65
  busy: busy ?? {},
64
66
  activeTab,
65
67
  onTabChange,
68
+ activeAttempt,
69
+ onAttemptChange,
66
70
  onSelect,
67
71
  onFiltersChange,
68
72
  onExpandAllChange,
@@ -82,6 +86,8 @@ function TestRunner({
82
86
  busy,
83
87
  activeTab,
84
88
  onTabChange,
89
+ activeAttempt,
90
+ onAttemptChange,
85
91
  onSelect,
86
92
  onFiltersChange,
87
93
  onExpandAllChange,
@@ -1 +1 @@
1
- {"version":3,"file":"TestRunner.cjs","sources":["../../../src/data/test-runner/TestRunner.tsx"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { SplitPane } from \"../../layout/SplitPane\";\nimport { cn } from \"../../lib/utils\";\nimport { UiArrowLeft, UiBeaker } from \"../../icons\";\nimport { useContainerWiderThan } from \"../../hooks/use-container-width\";\nimport type { RunMeta, SnapshotStatus, Test } from \"./types\";\nimport { aggregateStatusCounts, collectFrameworks } from \"./status\";\nimport { type TestFilters } from \"./filterState\";\nimport {\n createTestRunnerRegistry,\n type TestNodeAdapter,\n TestNodeAdapterRegistry,\n} from \"./adapter\";\nimport { TestRunnerProvider, type TestRunnerContext } from \"./context\";\nimport { TestTree } from \"./TestTree\";\nimport { TestDetailPanel } from \"./TestDetailPanel\";\nimport { TestRunSummary } from \"./TestRunSummary\";\nimport { TestFilterBar } from \"./TestFilterBar\";\n\nexport type TestRunnerProps = {\n tests: Test[];\n /**\n * The full, unfiltered forest driving the filter bar's pills/counts and the\n * header summary — falls back to `tests` when omitted (every existing\n * caller pre-filters `tests` itself). Pass this explicitly when `tests` is a\n * filtered subset of a larger report, so toggling a status filter (e.g.\n * excluding the only failed node) can't make its own pill — or the whole\n * filter bar, once every pill's count reads zero — disappear.\n */\n allTests?: Test[];\n selected: Test | null;\n filters: TestFilters;\n expandAll: boolean | null;\n done: boolean;\n runMeta?: RunMeta;\n status?: SnapshotStatus;\n /** Free-form status line shown next to the title (e.g. \"Running tests...\"). */\n statusText?: string;\n now?: number;\n busy?: { rerun?: boolean; stop?: boolean };\n\n /** Controlled active detail-tab id; omit to let the detail pane own tab state. */\n activeTab?: string;\n /** Called with the picked tab id when `activeTab` is controlled. */\n onTabChange?: (tabId: string) => void;\n\n onSelect: (node: Test | null) => void;\n onFiltersChange: (next: TestFilters) => void;\n onExpandAllChange: (next: boolean | null) => void;\n onRerun?: (node: Test) => void;\n onStop?: (node: Test) => void;\n\n /** Domain node adapters, or a pre-built registry. Defaults to default-only. */\n adapters?: TestNodeAdapter[] | TestNodeAdapterRegistry;\n /**\n * Header title. Defaults (when omitted) to the \"Test Results\" beaker heading.\n * Pass `null` to render no title — e.g. a host that owns the title in its own\n * dialog/page header and embeds the runner below it.\n */\n title?: ReactNode;\n /**\n * Render the built-in run summary (counts + progress) in the header. Defaults\n * to true; pass false when the host shows the summary itself (e.g. in a dialog\n * header) to avoid duplicating it.\n */\n showSummary?: boolean;\n /** Run actions at the right end of the one-line header (e.g. re-run, export, copy). */\n headerSlot?: ReactNode;\n /**\n * Suppresses the built-in header bar (title, summary) and the status/tool\n * facets in the tree toolbar.\n * Defaults to false; pass true when a host renders its own combined header\n * (e.g. a dialog header that already embeds `TestRunSummary` and filters) so\n * the runner doesn't render a second bordered bar underneath it.\n */\n hideHeader?: boolean;\n /** Initial left-pane width percentage. */\n defaultSplit?: number;\n startTime?: number | null;\n endTime?: number | null;\n className?: string;\n};\n\nfunction toRegistry(\n adapters: TestNodeAdapter[] | TestNodeAdapterRegistry | undefined,\n): TestNodeAdapterRegistry {\n if (adapters instanceof TestNodeAdapterRegistry) return adapters;\n return createTestRunnerRegistry(adapters ?? []);\n}\n\nconst SPLIT_MIN_WIDTH = 640;\n\n/**\n * Top-level, pure-presentational test runner: a summary/filter header over a\n * resizable tree + detail split. All data and handlers flow in via props and\n * are exposed to sub-components through a single `TestRunnerContext`. Domain\n * rendering is pluggable via node adapters; no fetch/SSE/routing lives here.\n */\nexport function TestRunner({\n tests,\n allTests,\n selected,\n filters,\n expandAll,\n done,\n runMeta,\n status,\n statusText,\n now,\n busy,\n activeTab,\n onTabChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters,\n title,\n showSummary = true,\n headerSlot,\n hideHeader = false,\n defaultSplit = 45,\n startTime,\n endTime,\n className,\n}: TestRunnerProps) {\n const registry = useMemo(() => toRegistry(adapters), [adapters]);\n\n const ctx: TestRunnerContext = useMemo(\n () => ({\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy: busy ?? {},\n activeTab,\n onTabChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters: registry,\n }),\n [\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy,\n activeTab,\n onTabChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n registry,\n ],\n );\n\n const summaryTests = allTests ?? tests;\n const frameworks = useMemo(() => collectFrameworks(summaryTests), [summaryTests]);\n const filterCounts = useMemo(() => aggregateStatusCounts(summaryTests), [summaryTests]);\n // A host that hides the header renders its own filters (see hideHeader).\n const filterBar =\n !hideHeader && summaryTests.length > 0 ? (\n <TestFilterBar\n filters={filters}\n onChange={onFiltersChange}\n counts={filterCounts}\n frameworks={frameworks}\n />\n ) : null;\n // Below this the split leaves both panes too narrow to read, so the runner\n // goes master/detail: the tree, then the selected test with a way back.\n const { ref, wider: roomy } = useContainerWiderThan(SPLIT_MIN_WIDTH);\n\n return (\n <TestRunnerProvider value={ctx}>\n <div ref={ref} className={cn(\"flex h-full min-h-0 flex-col bg-background\", className)}>\n {!hideHeader && (\n <div\n className=\"flex min-w-0 items-center gap-3 border-b border-border bg-card px-density-4 py-density-2\"\n data-runner-header=\"\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n {title === undefined ? (\n <h1 className=\"inline-flex shrink-0 items-center gap-1.5 text-base font-bold\">\n <Icon icon={UiBeaker} className=\"text-primary\" />\n Test Results\n </h1>\n ) : (\n title\n )}\n {statusText && (\n <span className=\"truncate text-sm text-muted-foreground\">{statusText}</span>\n )}\n </div>\n {showSummary && (\n <TestRunSummary\n tests={summaryTests}\n startTime={startTime}\n endTime={endTime}\n done={done}\n now={now}\n runMeta={runMeta}\n compact\n className=\"ml-auto flex-nowrap\"\n />\n )}\n {headerSlot && (\n <div className={cn(\"flex shrink-0 items-center gap-1\", !showSummary && \"ml-auto\")}>\n {headerSlot}\n </div>\n )}\n </div>\n )}\n\n {roomy ? (\n <SplitPane\n className=\"min-h-0 flex-1\"\n defaultSplit={defaultSplit}\n minLeft={25}\n minRight={30}\n leftClass=\"border-r-2 border-border bg-muted/30\"\n left={<TestTree actions={filterBar} />}\n right={<TestDetailPanel />}\n />\n ) : selected ? (\n <div className=\"flex min-h-0 flex-1 flex-col\">\n <button\n type=\"button\"\n onClick={() => onSelect(null)}\n className=\"inline-flex items-center gap-1.5 border-b border-border px-density-3 py-density-2 text-left text-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon icon={UiArrowLeft} />\n Back to results\n </button>\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <TestDetailPanel />\n </div>\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n <TestTree actions={filterBar} />\n </div>\n )}\n </div>\n </TestRunnerProvider>\n );\n}\n"],"names":["TestNodeAdapterRegistry","createTestRunnerRegistry","status","useMemo","collectFrameworks","aggregateStatusCounts","jsx","TestFilterBar","useContainerWiderThan","TestRunnerProvider","jsxs","cn","Icon","UiBeaker","TestRunSummary","SplitPane","TestTree","TestDetailPanel","UiArrowLeft"],"mappings":"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"}
1
+ {"version":3,"file":"TestRunner.cjs","sources":["../../../src/data/test-runner/TestRunner.tsx"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { SplitPane } from \"../../layout/SplitPane\";\nimport { cn } from \"../../lib/utils\";\nimport { UiArrowLeft, UiBeaker } from \"../../icons\";\nimport { useContainerWiderThan } from \"../../hooks/use-container-width\";\nimport type { RunMeta, SnapshotStatus, Test } from \"./types\";\nimport { aggregateStatusCounts, collectFrameworks } from \"./status\";\nimport { type TestFilters } from \"./filterState\";\nimport {\n createTestRunnerRegistry,\n type TestNodeAdapter,\n TestNodeAdapterRegistry,\n} from \"./adapter\";\nimport { TestRunnerProvider, type TestRunnerContext } from \"./context\";\nimport { TestTree } from \"./TestTree\";\nimport { TestDetailPanel } from \"./TestDetailPanel\";\nimport { TestRunSummary } from \"./TestRunSummary\";\nimport { TestFilterBar } from \"./TestFilterBar\";\n\nexport type TestRunnerProps = {\n tests: Test[];\n /**\n * The full, unfiltered forest driving the filter bar's pills/counts and the\n * header summary — falls back to `tests` when omitted (every existing\n * caller pre-filters `tests` itself). Pass this explicitly when `tests` is a\n * filtered subset of a larger report, so toggling a status filter (e.g.\n * excluding the only failed node) can't make its own pill — or the whole\n * filter bar, once every pill's count reads zero — disappear.\n */\n allTests?: Test[];\n selected: Test | null;\n filters: TestFilters;\n expandAll: boolean | null;\n done: boolean;\n runMeta?: RunMeta;\n status?: SnapshotStatus;\n /** Free-form status line shown next to the title (e.g. \"Running tests...\"). */\n statusText?: string;\n now?: number;\n busy?: { rerun?: boolean; stop?: boolean };\n\n /** Controlled active detail-tab id; omit to let the detail pane own tab state. */\n activeTab?: string;\n /** Called with the picked tab id when `activeTab` is controlled. */\n onTabChange?: (tabId: string) => void;\n activeAttempt?: number;\n onAttemptChange?: (attempt: number) => void;\n\n onSelect: (node: Test | null) => void;\n onFiltersChange: (next: TestFilters) => void;\n onExpandAllChange: (next: boolean | null) => void;\n onRerun?: (node: Test) => void;\n onStop?: (node: Test) => void;\n\n /** Domain node adapters, or a pre-built registry. Defaults to default-only. */\n adapters?: TestNodeAdapter[] | TestNodeAdapterRegistry;\n /**\n * Header title. Defaults (when omitted) to the \"Test Results\" beaker heading.\n * Pass `null` to render no title — e.g. a host that owns the title in its own\n * dialog/page header and embeds the runner below it.\n */\n title?: ReactNode;\n /**\n * Render the built-in run summary (counts + progress) in the header. Defaults\n * to true; pass false when the host shows the summary itself (e.g. in a dialog\n * header) to avoid duplicating it.\n */\n showSummary?: boolean;\n /** Run actions at the right end of the one-line header (e.g. re-run, export, copy). */\n headerSlot?: ReactNode;\n /**\n * Suppresses the built-in header bar (title, summary) and the status/tool\n * facets in the tree toolbar.\n * Defaults to false; pass true when a host renders its own combined header\n * (e.g. a dialog header that already embeds `TestRunSummary` and filters) so\n * the runner doesn't render a second bordered bar underneath it.\n */\n hideHeader?: boolean;\n /** Initial left-pane width percentage. */\n defaultSplit?: number;\n startTime?: number | null;\n endTime?: number | null;\n className?: string;\n};\n\nfunction toRegistry(\n adapters: TestNodeAdapter[] | TestNodeAdapterRegistry | undefined,\n): TestNodeAdapterRegistry {\n if (adapters instanceof TestNodeAdapterRegistry) return adapters;\n return createTestRunnerRegistry(adapters ?? []);\n}\n\nconst SPLIT_MIN_WIDTH = 640;\n\n/**\n * Top-level, pure-presentational test runner: a summary/filter header over a\n * resizable tree + detail split. All data and handlers flow in via props and\n * are exposed to sub-components through a single `TestRunnerContext`. Domain\n * rendering is pluggable via node adapters; no fetch/SSE/routing lives here.\n */\nexport function TestRunner({\n tests,\n allTests,\n selected,\n filters,\n expandAll,\n done,\n runMeta,\n status,\n statusText,\n now,\n busy,\n activeTab,\n onTabChange,\n activeAttempt,\n onAttemptChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters,\n title,\n showSummary = true,\n headerSlot,\n hideHeader = false,\n defaultSplit = 45,\n startTime,\n endTime,\n className,\n}: TestRunnerProps) {\n const registry = useMemo(() => toRegistry(adapters), [adapters]);\n\n const ctx: TestRunnerContext = useMemo(\n () => ({\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy: busy ?? {},\n activeTab,\n onTabChange,\n activeAttempt,\n onAttemptChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters: registry,\n }),\n [\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy,\n activeTab,\n onTabChange,\n activeAttempt,\n onAttemptChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n registry,\n ],\n );\n\n const summaryTests = allTests ?? tests;\n const frameworks = useMemo(() => collectFrameworks(summaryTests), [summaryTests]);\n const filterCounts = useMemo(() => aggregateStatusCounts(summaryTests), [summaryTests]);\n // A host that hides the header renders its own filters (see hideHeader).\n const filterBar =\n !hideHeader && summaryTests.length > 0 ? (\n <TestFilterBar\n filters={filters}\n onChange={onFiltersChange}\n counts={filterCounts}\n frameworks={frameworks}\n />\n ) : null;\n // Below this the split leaves both panes too narrow to read, so the runner\n // goes master/detail: the tree, then the selected test with a way back.\n const { ref, wider: roomy } = useContainerWiderThan(SPLIT_MIN_WIDTH);\n\n return (\n <TestRunnerProvider value={ctx}>\n <div ref={ref} className={cn(\"flex h-full min-h-0 flex-col bg-background\", className)}>\n {!hideHeader && (\n <div\n className=\"flex min-w-0 items-center gap-3 border-b border-border bg-card px-density-4 py-density-2\"\n data-runner-header=\"\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n {title === undefined ? (\n <h1 className=\"inline-flex shrink-0 items-center gap-1.5 text-base font-bold\">\n <Icon icon={UiBeaker} className=\"text-primary\" />\n Test Results\n </h1>\n ) : (\n title\n )}\n {statusText && (\n <span className=\"truncate text-sm text-muted-foreground\">{statusText}</span>\n )}\n </div>\n {showSummary && (\n <TestRunSummary\n tests={summaryTests}\n startTime={startTime}\n endTime={endTime}\n done={done}\n now={now}\n runMeta={runMeta}\n compact\n className=\"ml-auto flex-nowrap\"\n />\n )}\n {headerSlot && (\n <div className={cn(\"flex shrink-0 items-center gap-1\", !showSummary && \"ml-auto\")}>\n {headerSlot}\n </div>\n )}\n </div>\n )}\n\n {roomy ? (\n <SplitPane\n className=\"min-h-0 flex-1\"\n defaultSplit={defaultSplit}\n minLeft={25}\n minRight={30}\n leftClass=\"border-r-2 border-border bg-muted/30\"\n left={<TestTree actions={filterBar} />}\n right={<TestDetailPanel />}\n />\n ) : selected ? (\n <div className=\"flex min-h-0 flex-1 flex-col\">\n <button\n type=\"button\"\n onClick={() => onSelect(null)}\n className=\"inline-flex items-center gap-1.5 border-b border-border px-density-3 py-density-2 text-left text-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon icon={UiArrowLeft} />\n Back to results\n </button>\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <TestDetailPanel />\n </div>\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n <TestTree actions={filterBar} />\n </div>\n )}\n </div>\n </TestRunnerProvider>\n );\n}\n"],"names":["TestNodeAdapterRegistry","createTestRunnerRegistry","status","useMemo","collectFrameworks","aggregateStatusCounts","jsx","TestFilterBar","useContainerWiderThan","TestRunnerProvider","jsxs","cn","Icon","UiBeaker","TestRunSummary","SplitPane","TestTree","TestDetailPanel","UiArrowLeft"],"mappings":"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"}
@@ -30,6 +30,8 @@ export type TestRunnerProps = {
30
30
  activeTab?: string;
31
31
  /** Called with the picked tab id when `activeTab` is controlled. */
32
32
  onTabChange?: (tabId: string) => void;
33
+ activeAttempt?: number;
34
+ onAttemptChange?: (attempt: number) => void;
33
35
  onSelect: (node: Test | null) => void;
34
36
  onFiltersChange: (next: TestFilters) => void;
35
37
  onExpandAllChange: (next: boolean | null) => void;
@@ -71,5 +73,5 @@ export type TestRunnerProps = {
71
73
  * are exposed to sub-components through a single `TestRunnerContext`. Domain
72
74
  * rendering is pluggable via node adapters; no fetch/SSE/routing lives here.
73
75
  */
74
- export declare function TestRunner({ tests, allTests, selected, filters, expandAll, done, runMeta, status, statusText, now, busy, activeTab, onTabChange, onSelect, onFiltersChange, onExpandAllChange, onRerun, onStop, adapters, title, showSummary, headerSlot, hideHeader, defaultSplit, startTime, endTime, className, }: TestRunnerProps): import("react").JSX.Element;
76
+ export declare function TestRunner({ tests, allTests, selected, filters, expandAll, done, runMeta, status, statusText, now, busy, activeTab, onTabChange, activeAttempt, onAttemptChange, onSelect, onFiltersChange, onExpandAllChange, onRerun, onStop, adapters, title, showSummary, headerSlot, hideHeader, defaultSplit, startTime, endTime, className, }: TestRunnerProps): import("react").JSX.Element;
75
77
  //# sourceMappingURL=TestRunner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TestRunner.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestRunner.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAMhD,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE7D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAEL,KAAK,eAAe,EACpB,uBAAuB,EACxB,MAAM,WAAW,CAAC;AAOnB,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;IAClB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAE3C,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEtC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IACtC,eAAe,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IAC7C,iBAAiB,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC/B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAE9B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,eAAe,EAAE,GAAG,uBAAuB,CAAC;IACvD;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uFAAuF;IACvF,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAWF;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,SAAS,EACT,IAAI,EACJ,OAAO,EACP,MAAM,EACN,UAAU,EACV,GAAG,EACH,IAAI,EACJ,SAAS,EACT,WAAW,EACX,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,OAAO,EACP,MAAM,EACN,QAAQ,EACR,KAAK,EACL,WAAkB,EAClB,UAAU,EACV,UAAkB,EAClB,YAAiB,EACjB,SAAS,EACT,OAAO,EACP,SAAS,GACV,EAAE,eAAe,+BAsIjB"}
1
+ {"version":3,"file":"TestRunner.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestRunner.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAMhD,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE7D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAEL,KAAK,eAAe,EACpB,uBAAuB,EACxB,MAAM,WAAW,CAAC;AAOnB,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;IAClB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAE3C,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAE5C,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IACtC,eAAe,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IAC7C,iBAAiB,EAAE,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC/B,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAE9B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,eAAe,EAAE,GAAG,uBAAuB,CAAC;IACvD;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uFAAuF;IACvF,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAWF;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,SAAS,EACT,IAAI,EACJ,OAAO,EACP,MAAM,EACN,UAAU,EACV,GAAG,EACH,IAAI,EACJ,SAAS,EACT,WAAW,EACX,aAAa,EACb,eAAe,EACf,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,OAAO,EACP,MAAM,EACN,QAAQ,EACR,KAAK,EACL,WAAkB,EAClB,UAAU,EACV,UAAkB,EAClB,YAAiB,EACjB,SAAS,EACT,OAAO,EACP,SAAS,GACV,EAAE,eAAe,+BA0IjB"}
@@ -32,6 +32,8 @@ function TestRunner({
32
32
  busy,
33
33
  activeTab,
34
34
  onTabChange,
35
+ activeAttempt,
36
+ onAttemptChange,
35
37
  onSelect,
36
38
  onFiltersChange,
37
39
  onExpandAllChange,
@@ -61,6 +63,8 @@ function TestRunner({
61
63
  busy: busy ?? {},
62
64
  activeTab,
63
65
  onTabChange,
66
+ activeAttempt,
67
+ onAttemptChange,
64
68
  onSelect,
65
69
  onFiltersChange,
66
70
  onExpandAllChange,
@@ -80,6 +84,8 @@ function TestRunner({
80
84
  busy,
81
85
  activeTab,
82
86
  onTabChange,
87
+ activeAttempt,
88
+ onAttemptChange,
83
89
  onSelect,
84
90
  onFiltersChange,
85
91
  onExpandAllChange,
@@ -1 +1 @@
1
- {"version":3,"file":"TestRunner.js","sources":["../../../src/data/test-runner/TestRunner.tsx"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { SplitPane } from \"../../layout/SplitPane\";\nimport { cn } from \"../../lib/utils\";\nimport { UiArrowLeft, UiBeaker } from \"../../icons\";\nimport { useContainerWiderThan } from \"../../hooks/use-container-width\";\nimport type { RunMeta, SnapshotStatus, Test } from \"./types\";\nimport { aggregateStatusCounts, collectFrameworks } from \"./status\";\nimport { type TestFilters } from \"./filterState\";\nimport {\n createTestRunnerRegistry,\n type TestNodeAdapter,\n TestNodeAdapterRegistry,\n} from \"./adapter\";\nimport { TestRunnerProvider, type TestRunnerContext } from \"./context\";\nimport { TestTree } from \"./TestTree\";\nimport { TestDetailPanel } from \"./TestDetailPanel\";\nimport { TestRunSummary } from \"./TestRunSummary\";\nimport { TestFilterBar } from \"./TestFilterBar\";\n\nexport type TestRunnerProps = {\n tests: Test[];\n /**\n * The full, unfiltered forest driving the filter bar's pills/counts and the\n * header summary — falls back to `tests` when omitted (every existing\n * caller pre-filters `tests` itself). Pass this explicitly when `tests` is a\n * filtered subset of a larger report, so toggling a status filter (e.g.\n * excluding the only failed node) can't make its own pill — or the whole\n * filter bar, once every pill's count reads zero — disappear.\n */\n allTests?: Test[];\n selected: Test | null;\n filters: TestFilters;\n expandAll: boolean | null;\n done: boolean;\n runMeta?: RunMeta;\n status?: SnapshotStatus;\n /** Free-form status line shown next to the title (e.g. \"Running tests...\"). */\n statusText?: string;\n now?: number;\n busy?: { rerun?: boolean; stop?: boolean };\n\n /** Controlled active detail-tab id; omit to let the detail pane own tab state. */\n activeTab?: string;\n /** Called with the picked tab id when `activeTab` is controlled. */\n onTabChange?: (tabId: string) => void;\n\n onSelect: (node: Test | null) => void;\n onFiltersChange: (next: TestFilters) => void;\n onExpandAllChange: (next: boolean | null) => void;\n onRerun?: (node: Test) => void;\n onStop?: (node: Test) => void;\n\n /** Domain node adapters, or a pre-built registry. Defaults to default-only. */\n adapters?: TestNodeAdapter[] | TestNodeAdapterRegistry;\n /**\n * Header title. Defaults (when omitted) to the \"Test Results\" beaker heading.\n * Pass `null` to render no title — e.g. a host that owns the title in its own\n * dialog/page header and embeds the runner below it.\n */\n title?: ReactNode;\n /**\n * Render the built-in run summary (counts + progress) in the header. Defaults\n * to true; pass false when the host shows the summary itself (e.g. in a dialog\n * header) to avoid duplicating it.\n */\n showSummary?: boolean;\n /** Run actions at the right end of the one-line header (e.g. re-run, export, copy). */\n headerSlot?: ReactNode;\n /**\n * Suppresses the built-in header bar (title, summary) and the status/tool\n * facets in the tree toolbar.\n * Defaults to false; pass true when a host renders its own combined header\n * (e.g. a dialog header that already embeds `TestRunSummary` and filters) so\n * the runner doesn't render a second bordered bar underneath it.\n */\n hideHeader?: boolean;\n /** Initial left-pane width percentage. */\n defaultSplit?: number;\n startTime?: number | null;\n endTime?: number | null;\n className?: string;\n};\n\nfunction toRegistry(\n adapters: TestNodeAdapter[] | TestNodeAdapterRegistry | undefined,\n): TestNodeAdapterRegistry {\n if (adapters instanceof TestNodeAdapterRegistry) return adapters;\n return createTestRunnerRegistry(adapters ?? []);\n}\n\nconst SPLIT_MIN_WIDTH = 640;\n\n/**\n * Top-level, pure-presentational test runner: a summary/filter header over a\n * resizable tree + detail split. All data and handlers flow in via props and\n * are exposed to sub-components through a single `TestRunnerContext`. Domain\n * rendering is pluggable via node adapters; no fetch/SSE/routing lives here.\n */\nexport function TestRunner({\n tests,\n allTests,\n selected,\n filters,\n expandAll,\n done,\n runMeta,\n status,\n statusText,\n now,\n busy,\n activeTab,\n onTabChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters,\n title,\n showSummary = true,\n headerSlot,\n hideHeader = false,\n defaultSplit = 45,\n startTime,\n endTime,\n className,\n}: TestRunnerProps) {\n const registry = useMemo(() => toRegistry(adapters), [adapters]);\n\n const ctx: TestRunnerContext = useMemo(\n () => ({\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy: busy ?? {},\n activeTab,\n onTabChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters: registry,\n }),\n [\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy,\n activeTab,\n onTabChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n registry,\n ],\n );\n\n const summaryTests = allTests ?? tests;\n const frameworks = useMemo(() => collectFrameworks(summaryTests), [summaryTests]);\n const filterCounts = useMemo(() => aggregateStatusCounts(summaryTests), [summaryTests]);\n // A host that hides the header renders its own filters (see hideHeader).\n const filterBar =\n !hideHeader && summaryTests.length > 0 ? (\n <TestFilterBar\n filters={filters}\n onChange={onFiltersChange}\n counts={filterCounts}\n frameworks={frameworks}\n />\n ) : null;\n // Below this the split leaves both panes too narrow to read, so the runner\n // goes master/detail: the tree, then the selected test with a way back.\n const { ref, wider: roomy } = useContainerWiderThan(SPLIT_MIN_WIDTH);\n\n return (\n <TestRunnerProvider value={ctx}>\n <div ref={ref} className={cn(\"flex h-full min-h-0 flex-col bg-background\", className)}>\n {!hideHeader && (\n <div\n className=\"flex min-w-0 items-center gap-3 border-b border-border bg-card px-density-4 py-density-2\"\n data-runner-header=\"\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n {title === undefined ? (\n <h1 className=\"inline-flex shrink-0 items-center gap-1.5 text-base font-bold\">\n <Icon icon={UiBeaker} className=\"text-primary\" />\n Test Results\n </h1>\n ) : (\n title\n )}\n {statusText && (\n <span className=\"truncate text-sm text-muted-foreground\">{statusText}</span>\n )}\n </div>\n {showSummary && (\n <TestRunSummary\n tests={summaryTests}\n startTime={startTime}\n endTime={endTime}\n done={done}\n now={now}\n runMeta={runMeta}\n compact\n className=\"ml-auto flex-nowrap\"\n />\n )}\n {headerSlot && (\n <div className={cn(\"flex shrink-0 items-center gap-1\", !showSummary && \"ml-auto\")}>\n {headerSlot}\n </div>\n )}\n </div>\n )}\n\n {roomy ? (\n <SplitPane\n className=\"min-h-0 flex-1\"\n defaultSplit={defaultSplit}\n minLeft={25}\n minRight={30}\n leftClass=\"border-r-2 border-border bg-muted/30\"\n left={<TestTree actions={filterBar} />}\n right={<TestDetailPanel />}\n />\n ) : selected ? (\n <div className=\"flex min-h-0 flex-1 flex-col\">\n <button\n type=\"button\"\n onClick={() => onSelect(null)}\n className=\"inline-flex items-center gap-1.5 border-b border-border px-density-3 py-density-2 text-left text-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon icon={UiArrowLeft} />\n Back to results\n </button>\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <TestDetailPanel />\n </div>\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n <TestTree actions={filterBar} />\n </div>\n )}\n </div>\n </TestRunnerProvider>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TestRunner.js","sources":["../../../src/data/test-runner/TestRunner.tsx"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { SplitPane } from \"../../layout/SplitPane\";\nimport { cn } from \"../../lib/utils\";\nimport { UiArrowLeft, UiBeaker } from \"../../icons\";\nimport { useContainerWiderThan } from \"../../hooks/use-container-width\";\nimport type { RunMeta, SnapshotStatus, Test } from \"./types\";\nimport { aggregateStatusCounts, collectFrameworks } from \"./status\";\nimport { type TestFilters } from \"./filterState\";\nimport {\n createTestRunnerRegistry,\n type TestNodeAdapter,\n TestNodeAdapterRegistry,\n} from \"./adapter\";\nimport { TestRunnerProvider, type TestRunnerContext } from \"./context\";\nimport { TestTree } from \"./TestTree\";\nimport { TestDetailPanel } from \"./TestDetailPanel\";\nimport { TestRunSummary } from \"./TestRunSummary\";\nimport { TestFilterBar } from \"./TestFilterBar\";\n\nexport type TestRunnerProps = {\n tests: Test[];\n /**\n * The full, unfiltered forest driving the filter bar's pills/counts and the\n * header summary — falls back to `tests` when omitted (every existing\n * caller pre-filters `tests` itself). Pass this explicitly when `tests` is a\n * filtered subset of a larger report, so toggling a status filter (e.g.\n * excluding the only failed node) can't make its own pill — or the whole\n * filter bar, once every pill's count reads zero — disappear.\n */\n allTests?: Test[];\n selected: Test | null;\n filters: TestFilters;\n expandAll: boolean | null;\n done: boolean;\n runMeta?: RunMeta;\n status?: SnapshotStatus;\n /** Free-form status line shown next to the title (e.g. \"Running tests...\"). */\n statusText?: string;\n now?: number;\n busy?: { rerun?: boolean; stop?: boolean };\n\n /** Controlled active detail-tab id; omit to let the detail pane own tab state. */\n activeTab?: string;\n /** Called with the picked tab id when `activeTab` is controlled. */\n onTabChange?: (tabId: string) => void;\n activeAttempt?: number;\n onAttemptChange?: (attempt: number) => void;\n\n onSelect: (node: Test | null) => void;\n onFiltersChange: (next: TestFilters) => void;\n onExpandAllChange: (next: boolean | null) => void;\n onRerun?: (node: Test) => void;\n onStop?: (node: Test) => void;\n\n /** Domain node adapters, or a pre-built registry. Defaults to default-only. */\n adapters?: TestNodeAdapter[] | TestNodeAdapterRegistry;\n /**\n * Header title. Defaults (when omitted) to the \"Test Results\" beaker heading.\n * Pass `null` to render no title — e.g. a host that owns the title in its own\n * dialog/page header and embeds the runner below it.\n */\n title?: ReactNode;\n /**\n * Render the built-in run summary (counts + progress) in the header. Defaults\n * to true; pass false when the host shows the summary itself (e.g. in a dialog\n * header) to avoid duplicating it.\n */\n showSummary?: boolean;\n /** Run actions at the right end of the one-line header (e.g. re-run, export, copy). */\n headerSlot?: ReactNode;\n /**\n * Suppresses the built-in header bar (title, summary) and the status/tool\n * facets in the tree toolbar.\n * Defaults to false; pass true when a host renders its own combined header\n * (e.g. a dialog header that already embeds `TestRunSummary` and filters) so\n * the runner doesn't render a second bordered bar underneath it.\n */\n hideHeader?: boolean;\n /** Initial left-pane width percentage. */\n defaultSplit?: number;\n startTime?: number | null;\n endTime?: number | null;\n className?: string;\n};\n\nfunction toRegistry(\n adapters: TestNodeAdapter[] | TestNodeAdapterRegistry | undefined,\n): TestNodeAdapterRegistry {\n if (adapters instanceof TestNodeAdapterRegistry) return adapters;\n return createTestRunnerRegistry(adapters ?? []);\n}\n\nconst SPLIT_MIN_WIDTH = 640;\n\n/**\n * Top-level, pure-presentational test runner: a summary/filter header over a\n * resizable tree + detail split. All data and handlers flow in via props and\n * are exposed to sub-components through a single `TestRunnerContext`. Domain\n * rendering is pluggable via node adapters; no fetch/SSE/routing lives here.\n */\nexport function TestRunner({\n tests,\n allTests,\n selected,\n filters,\n expandAll,\n done,\n runMeta,\n status,\n statusText,\n now,\n busy,\n activeTab,\n onTabChange,\n activeAttempt,\n onAttemptChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters,\n title,\n showSummary = true,\n headerSlot,\n hideHeader = false,\n defaultSplit = 45,\n startTime,\n endTime,\n className,\n}: TestRunnerProps) {\n const registry = useMemo(() => toRegistry(adapters), [adapters]);\n\n const ctx: TestRunnerContext = useMemo(\n () => ({\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy: busy ?? {},\n activeTab,\n onTabChange,\n activeAttempt,\n onAttemptChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n adapters: registry,\n }),\n [\n tests,\n selected,\n runMeta,\n status,\n done,\n now,\n filters,\n expandAll,\n busy,\n activeTab,\n onTabChange,\n activeAttempt,\n onAttemptChange,\n onSelect,\n onFiltersChange,\n onExpandAllChange,\n onRerun,\n onStop,\n registry,\n ],\n );\n\n const summaryTests = allTests ?? tests;\n const frameworks = useMemo(() => collectFrameworks(summaryTests), [summaryTests]);\n const filterCounts = useMemo(() => aggregateStatusCounts(summaryTests), [summaryTests]);\n // A host that hides the header renders its own filters (see hideHeader).\n const filterBar =\n !hideHeader && summaryTests.length > 0 ? (\n <TestFilterBar\n filters={filters}\n onChange={onFiltersChange}\n counts={filterCounts}\n frameworks={frameworks}\n />\n ) : null;\n // Below this the split leaves both panes too narrow to read, so the runner\n // goes master/detail: the tree, then the selected test with a way back.\n const { ref, wider: roomy } = useContainerWiderThan(SPLIT_MIN_WIDTH);\n\n return (\n <TestRunnerProvider value={ctx}>\n <div ref={ref} className={cn(\"flex h-full min-h-0 flex-col bg-background\", className)}>\n {!hideHeader && (\n <div\n className=\"flex min-w-0 items-center gap-3 border-b border-border bg-card px-density-4 py-density-2\"\n data-runner-header=\"\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n {title === undefined ? (\n <h1 className=\"inline-flex shrink-0 items-center gap-1.5 text-base font-bold\">\n <Icon icon={UiBeaker} className=\"text-primary\" />\n Test Results\n </h1>\n ) : (\n title\n )}\n {statusText && (\n <span className=\"truncate text-sm text-muted-foreground\">{statusText}</span>\n )}\n </div>\n {showSummary && (\n <TestRunSummary\n tests={summaryTests}\n startTime={startTime}\n endTime={endTime}\n done={done}\n now={now}\n runMeta={runMeta}\n compact\n className=\"ml-auto flex-nowrap\"\n />\n )}\n {headerSlot && (\n <div className={cn(\"flex shrink-0 items-center gap-1\", !showSummary && \"ml-auto\")}>\n {headerSlot}\n </div>\n )}\n </div>\n )}\n\n {roomy ? (\n <SplitPane\n className=\"min-h-0 flex-1\"\n defaultSplit={defaultSplit}\n minLeft={25}\n minRight={30}\n leftClass=\"border-r-2 border-border bg-muted/30\"\n left={<TestTree actions={filterBar} />}\n right={<TestDetailPanel />}\n />\n ) : selected ? (\n <div className=\"flex min-h-0 flex-1 flex-col\">\n <button\n type=\"button\"\n onClick={() => onSelect(null)}\n className=\"inline-flex items-center gap-1.5 border-b border-border px-density-3 py-density-2 text-left text-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon icon={UiArrowLeft} />\n Back to results\n </button>\n <div className=\"min-h-0 flex-1 overflow-hidden\">\n <TestDetailPanel />\n </div>\n </div>\n ) : (\n <div className=\"min-h-0 flex-1 overflow-y-auto\">\n <TestTree actions={filterBar} />\n </div>\n )}\n </div>\n </TestRunnerProvider>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ const IconButton = require("../../components/IconButton.cjs");
7
7
  const utils = require("../../lib/utils.cjs");
8
8
  const context = require("./context.cjs");
9
9
  const status = require("./status.cjs");
10
+ const marks = require("./marks.cjs");
10
11
  const MarkIcons = require("./MarkIcons.cjs");
11
12
  const UiClass = require("../../icons/components/UiClass.cjs");
12
13
  const UiStop = require("../../icons/components/UiStop.cjs");
@@ -49,7 +50,7 @@ function TestTreeNode({ node, selected }) {
49
50
  const message = status$1 && MESSAGE_STATUSES.has(status$1) ? (node.message ?? "").trim() : "";
50
51
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
51
52
  outcome ? /* @__PURE__ */ jsxRuntime.jsx(MarkIcons.OutcomeIcon, { outcome }) : /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiClass.UiClass, className: "shrink-0 text-muted-foreground" }),
52
- fwNode != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center text-sm", children: fwNode }) : /* @__PURE__ */ jsxRuntime.jsx(MarkIcons.ToolIcon, { tool: node.framework, className: "text-sm" }),
53
+ fwNode != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center text-sm", children: fwNode }) : /* @__PURE__ */ jsxRuntime.jsx(MarkIcons.ToolIcon, { tool: marks.toolKeyFor(node), className: "text-sm" }),
53
54
  (_c = leadingAdapter == null ? void 0 : leadingAdapter.renderRowLeading) == null ? void 0 : _c.call(leadingAdapter, { node, runner }),
54
55
  /* @__PURE__ */ jsxRuntime.jsx(
55
56
  "span",
@@ -1 +1 @@
1
- {"version":3,"file":"TestTreeNode.cjs","sources":["../../../src/data/test-runner/TestTreeNode.tsx"],"sourcesContent":["import { Icon } from \"../Icon\";\nimport { Badge } from \"../Badge\";\nimport { IconButton } from \"../../components/IconButton\";\nimport { UiClass, UiRefresh, UiStop } from \"../../icons\";\nimport { cn } from \"../../lib/utils\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport {\n formatTestDuration,\n humanizeName,\n outcomeFor,\n sum,\n testStatus,\n totalDuration,\n} from \"./status\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\n\nconst NAME_TONE: Record<string, string> = {\n failed: \"text-red-600 dark:text-red-400\",\n timedout: \"text-amber-600 dark:text-amber-400\",\n warned: \"text-amber-600 dark:text-amber-400\",\n running: \"text-blue-600 dark:text-blue-400\",\n skipped: \"text-yellow-700 dark:text-yellow-500\",\n pending: \"text-muted-foreground\",\n passed: \"text-foreground\",\n};\n\nconst MESSAGE_STATUSES = new Set([\"failed\", \"timedout\", \"warned\", \"skipped\"]);\n\n/** Per-status roll-up count badges shown on a container row. */\nfunction CountBadges({ node }: { node: Test }) {\n const s = sum(node);\n if (s.total === 0) return null;\n return (\n <span className=\"flex shrink-0 items-center gap-1\">\n {s.passed > 0 && <Badge tone=\"success\" variant=\"solid\" size=\"xxs\" count={s.passed} />}\n {s.failed > 0 && <Badge tone=\"danger\" variant=\"solid\" size=\"xxs\" count={s.failed} />}\n {s.warned > 0 && <Badge tone=\"warning\" variant=\"outline\" size=\"xxs\" count={s.warned} />}\n {s.timedout > 0 && <Badge tone=\"warning\" variant=\"solid\" size=\"xxs\" count={s.timedout} />}\n {s.skipped > 0 && <Badge tone=\"warning\" variant=\"soft\" size=\"xxs\" count={s.skipped} />}\n {s.running > 0 && <Badge tone=\"info\" variant=\"solid\" size=\"xxs\" count={s.running} />}\n {s.pending > 0 && <Badge tone=\"neutral\" variant=\"soft\" size=\"xxs\" count={s.pending} />}\n </span>\n );\n}\n\n/**\n * Renders one test tree row: status icon, optional adapter decoration + framework\n * icon, status-colored name, duration, per-status count badges, and (when the\n * runner exposes the handlers) rerun/stop buttons. Designed as the `renderRow`\n * for the clicky `Tree`.\n */\nexport function TestTreeNode({ node, selected }: { node: Test; selected: boolean }) {\n const runner = useTestRunner();\n const hasChildren = (node.children?.length ?? 0) > 0;\n const status = testStatus(node);\n const outcome = outcomeFor(node);\n const fwAdapter = runner.adapters.resolveFrameworkIcon(node, runner);\n const fwNode = fwAdapter?.renderFrameworkIcon?.({ node, runner });\n const leadingAdapter = runner.adapters.resolveRowLeading(node, runner);\n const dur = node.duration || (hasChildren ? totalDuration(node) : 0);\n const canRerun = !!runner.onRerun && node.framework !== \"task\";\n const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;\n // A verdict line under the name for rows that need explaining; green,\n // running, and queued rows stay single-line.\n const message = status && MESSAGE_STATUSES.has(status) ? (node.message ?? \"\").trim() : \"\";\n\n return (\n <>\n {outcome ? (\n <OutcomeIcon outcome={outcome} />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-muted-foreground\" />\n )}\n\n {fwNode != null ? (\n <span className=\"flex shrink-0 items-center text-sm\">{fwNode}</span>\n ) : (\n <ToolIcon tool={node.framework} className=\"text-sm\" />\n )}\n\n {leadingAdapter?.renderRowLeading?.({ node, runner })}\n\n <span\n className={cn(\n \"min-w-0 truncate\",\n selected ? \"font-semibold\" : \"font-medium\",\n (status && NAME_TONE[status]) || \"text-foreground\",\n )}\n title={message || undefined}\n >\n {humanizeName(node.name, node.framework)}\n </span>\n {/* The verdict trails the name on the same line and gives way first. */}\n {message ? (\n <span\n className=\"min-w-0 flex-1 shrink-[100] truncate text-xs text-muted-foreground\"\n title={message}\n data-row-message=\"\"\n >\n {message}\n </span>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {node.progress && node.progress.total > 0 && !node.passed && !node.failed && (\n <span className=\"flex shrink-0 items-center gap-1 text-xs text-blue-600 dark:text-blue-400\">\n {node.progress.phase && <span>{node.progress.phase}</span>}\n <span className=\"tabular-nums\">\n {node.progress.done}/{node.progress.total}\n </span>\n </span>\n )}\n\n {dur > 0 && (\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatTestDuration(dur)}</span>\n )}\n\n {hasChildren && <CountBadges node={node} />}\n\n {canStop && (\n <IconButton\n icon={UiStop}\n label=\"Stop\"\n disabled={runner.busy.stop}\n onClick={(e) => {\n e.stopPropagation();\n runner.onStop!(node);\n }}\n />\n )}\n\n {canRerun && (\n <IconButton\n icon={UiRefresh}\n label=\"Rerun\"\n disabled={runner.busy.rerun}\n onClick={(e) => {\n e.stopPropagation();\n runner.onRerun!(node);\n }}\n />\n )}\n </>\n );\n}\n"],"names":["sum","jsxs","jsx","Badge","useTestRunner","status","testStatus","outcomeFor","totalDuration","Fragment","OutcomeIcon","Icon","UiClass","ToolIcon","cn","humanizeName","formatTestDuration","IconButton","UiStop","UiRefresh"],"mappings":";;;;;;;;;;;;;AAiBA,MAAM,YAAoC;AAAA,EACxC,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AACV;AAEA,MAAM,uCAAuB,IAAI,CAAC,UAAU,YAAY,UAAU,SAAS,CAAC;AAG5E,SAAS,YAAY,EAAE,QAAwB;AAC7C,QAAM,IAAIA,OAAAA,IAAI,IAAI;AAClB,MAAI,EAAE,UAAU,EAAG,QAAO;AAC1B,SACEC,2BAAAA,KAAC,QAAA,EAAK,WAAU,oCACb,UAAA;AAAA,IAAA,EAAE,SAAS,KAAKC,2BAAAA,IAACC,MAAAA,OAAA,EAAM,MAAK,WAAU,SAAQ,SAAQ,MAAK,OAAM,OAAO,EAAE,QAAQ;AAAA,IAClF,EAAE,SAAS,KAAKD,2BAAAA,IAACC,MAAAA,OAAA,EAAM,MAAK,UAAS,SAAQ,SAAQ,MAAK,OAAM,OAAO,EAAE,QAAQ;AAAA,IACjF,EAAE,SAAS,KAAKD,2BAAAA,IAACC,MAAAA,OAAA,EAAM,MAAK,WAAU,SAAQ,WAAU,MAAK,OAAM,OAAO,EAAE,QAAQ;AAAA,IACpF,EAAE,WAAW,KAAKD,2BAAAA,IAACC,MAAAA,OAAA,EAAM,MAAK,WAAU,SAAQ,SAAQ,MAAK,OAAM,OAAO,EAAE,UAAU;AAAA,IACtF,EAAE,UAAU,KAAKD,2BAAAA,IAACC,MAAAA,OAAA,EAAM,MAAK,WAAU,SAAQ,QAAO,MAAK,OAAM,OAAO,EAAE,SAAS;AAAA,IACnF,EAAE,UAAU,KAAKD,2BAAAA,IAACC,MAAAA,OAAA,EAAM,MAAK,QAAO,SAAQ,SAAQ,MAAK,OAAM,OAAO,EAAE,SAAS;AAAA,IACjF,EAAE,UAAU,KAAKD,2BAAAA,IAACC,MAAAA,OAAA,EAAM,MAAK,WAAU,SAAQ,QAAO,MAAK,OAAM,OAAO,EAAE,QAAA,CAAS;AAAA,EAAA,GACtF;AAEJ;AAQO,SAAS,aAAa,EAAE,MAAM,YAA+C;;AAClF,QAAM,SAASC,QAAAA,cAAA;AACf,QAAM,iBAAe,UAAK,aAAL,mBAAe,WAAU,KAAK;AACnD,QAAMC,WAASC,OAAAA,WAAW,IAAI;AAC9B,QAAM,UAAUC,OAAAA,WAAW,IAAI;AAC/B,QAAM,YAAY,OAAO,SAAS,qBAAqB,MAAM,MAAM;AACnE,QAAM,UAAS,4CAAW,wBAAX,mCAAiC,EAAE,MAAM;AACxD,QAAM,iBAAiB,OAAO,SAAS,kBAAkB,MAAM,MAAM;AACrE,QAAM,MAAM,KAAK,aAAa,cAAcC,qBAAc,IAAI,IAAI;AAClE,QAAM,WAAW,CAAC,CAAC,OAAO,WAAW,KAAK,cAAc;AACxD,QAAM,UAAU,CAAC,CAAC,OAAO,UAAU,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,KAAK;AAG7D,QAAM,UAAUH,YAAU,iBAAiB,IAAIA,QAAM,KAAK,KAAK,WAAW,IAAI,KAAA,IAAS;AAEvF,SACEJ,2BAAAA,KAAAQ,qBAAA,EACG,UAAA;AAAA,IAAA,UACCP,2BAAAA,IAACQ,UAAAA,eAAY,QAAA,CAAkB,mCAE9BC,KAAAA,MAAA,EAAK,MAAMC,QAAAA,SAAS,WAAU,iCAAA,CAAiC;AAAA,IAGjE,UAAU,OACTV,+BAAC,QAAA,EAAK,WAAU,sCAAsC,UAAA,OAAA,CAAO,IAE7DA,2BAAAA,IAACW,UAAAA,UAAA,EAAS,MAAM,KAAK,WAAW,WAAU,WAAU;AAAA,KAGrD,sDAAgB,qBAAhB,wCAAmC,EAAE,MAAM;IAE5CX,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAWY,MAAAA;AAAAA,UACT;AAAA,UACA,WAAW,kBAAkB;AAAA,UAC5BT,YAAU,UAAUA,QAAM,KAAM;AAAA,QAAA;AAAA,QAEnC,OAAO,WAAW;AAAA,QAEjB,UAAAU,OAAAA,aAAa,KAAK,MAAM,KAAK,SAAS;AAAA,MAAA;AAAA,IAAA;AAAA,IAGxC,UACCb,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO;AAAA,QACP,oBAAiB;AAAA,QAEhB,UAAA;AAAA,MAAA;AAAA,IAAA,IAGHA,2BAAAA,IAAC,QAAA,EAAK,WAAU,SAAA,CAAS;AAAA,IAG1B,KAAK,YAAY,KAAK,SAAS,QAAQ,KAAK,CAAC,KAAK,UAAU,CAAC,KAAK,UACjED,gCAAC,QAAA,EAAK,WAAU,6EACb,UAAA;AAAA,MAAA,KAAK,SAAS,SAASC,+BAAC,QAAA,EAAM,UAAA,KAAK,SAAS,OAAM;AAAA,MACnDD,2BAAAA,KAAC,QAAA,EAAK,WAAU,gBACb,UAAA;AAAA,QAAA,KAAK,SAAS;AAAA,QAAK;AAAA,QAAE,KAAK,SAAS;AAAA,MAAA,EAAA,CACtC;AAAA,IAAA,GACF;AAAA,IAGD,MAAM,KACLC,+BAAC,QAAA,EAAK,WAAU,0CAA0C,UAAAc,OAAAA,mBAAmB,GAAG,GAAE;AAAA,IAGnF,eAAed,2BAAAA,IAAC,aAAA,EAAY,KAAA,CAAY;AAAA,IAExC,WACCA,2BAAAA;AAAAA,MAACe,WAAAA;AAAAA,MAAA;AAAA,QACC,MAAMC,OAAAA;AAAAA,QACN,OAAM;AAAA,QACN,UAAU,OAAO,KAAK;AAAA,QACtB,SAAS,CAAC,MAAM;AACd,YAAE,gBAAA;AACF,iBAAO,OAAQ,IAAI;AAAA,QACrB;AAAA,MAAA;AAAA,IAAA;AAAA,IAIH,YACChB,2BAAAA;AAAAA,MAACe,WAAAA;AAAAA,MAAA;AAAA,QACC,MAAME,UAAAA;AAAAA,QACN,OAAM;AAAA,QACN,UAAU,OAAO,KAAK;AAAA,QACtB,SAAS,CAAC,MAAM;AACd,YAAE,gBAAA;AACF,iBAAO,QAAS,IAAI;AAAA,QACtB;AAAA,MAAA;AAAA,IAAA;AAAA,EACF,GAEJ;AAEJ;;"}
1
+ {"version":3,"file":"TestTreeNode.cjs","sources":["../../../src/data/test-runner/TestTreeNode.tsx"],"sourcesContent":["import { Icon } from \"../Icon\";\nimport { Badge } from \"../Badge\";\nimport { IconButton } from \"../../components/IconButton\";\nimport { UiClass, UiRefresh, UiStop } from \"../../icons\";\nimport { cn } from \"../../lib/utils\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport {\n formatTestDuration,\n humanizeName,\n outcomeFor,\n sum,\n testStatus,\n totalDuration,\n} from \"./status\";\nimport { toolKeyFor } from \"./marks\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\n\nconst NAME_TONE: Record<string, string> = {\n failed: \"text-red-600 dark:text-red-400\",\n timedout: \"text-amber-600 dark:text-amber-400\",\n warned: \"text-amber-600 dark:text-amber-400\",\n running: \"text-blue-600 dark:text-blue-400\",\n skipped: \"text-yellow-700 dark:text-yellow-500\",\n pending: \"text-muted-foreground\",\n passed: \"text-foreground\",\n};\n\nconst MESSAGE_STATUSES = new Set([\"failed\", \"timedout\", \"warned\", \"skipped\"]);\n\n/** Per-status roll-up count badges shown on a container row. */\nfunction CountBadges({ node }: { node: Test }) {\n const s = sum(node);\n if (s.total === 0) return null;\n return (\n <span className=\"flex shrink-0 items-center gap-1\">\n {s.passed > 0 && <Badge tone=\"success\" variant=\"solid\" size=\"xxs\" count={s.passed} />}\n {s.failed > 0 && <Badge tone=\"danger\" variant=\"solid\" size=\"xxs\" count={s.failed} />}\n {s.warned > 0 && <Badge tone=\"warning\" variant=\"outline\" size=\"xxs\" count={s.warned} />}\n {s.timedout > 0 && <Badge tone=\"warning\" variant=\"solid\" size=\"xxs\" count={s.timedout} />}\n {s.skipped > 0 && <Badge tone=\"warning\" variant=\"soft\" size=\"xxs\" count={s.skipped} />}\n {s.running > 0 && <Badge tone=\"info\" variant=\"solid\" size=\"xxs\" count={s.running} />}\n {s.pending > 0 && <Badge tone=\"neutral\" variant=\"soft\" size=\"xxs\" count={s.pending} />}\n </span>\n );\n}\n\n/**\n * Renders one test tree row: status icon, optional adapter decoration + framework\n * icon, status-colored name, duration, per-status count badges, and (when the\n * runner exposes the handlers) rerun/stop buttons. Designed as the `renderRow`\n * for the clicky `Tree`.\n */\nexport function TestTreeNode({ node, selected }: { node: Test; selected: boolean }) {\n const runner = useTestRunner();\n const hasChildren = (node.children?.length ?? 0) > 0;\n const status = testStatus(node);\n const outcome = outcomeFor(node);\n const fwAdapter = runner.adapters.resolveFrameworkIcon(node, runner);\n const fwNode = fwAdapter?.renderFrameworkIcon?.({ node, runner });\n const leadingAdapter = runner.adapters.resolveRowLeading(node, runner);\n const dur = node.duration || (hasChildren ? totalDuration(node) : 0);\n const canRerun = !!runner.onRerun && node.framework !== \"task\";\n const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;\n // A verdict line under the name for rows that need explaining; green,\n // running, and queued rows stay single-line.\n const message = status && MESSAGE_STATUSES.has(status) ? (node.message ?? \"\").trim() : \"\";\n\n return (\n <>\n {outcome ? (\n <OutcomeIcon outcome={outcome} />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-muted-foreground\" />\n )}\n\n {fwNode != null ? (\n <span className=\"flex shrink-0 items-center text-sm\">{fwNode}</span>\n ) : (\n <ToolIcon tool={toolKeyFor(node)} className=\"text-sm\" />\n )}\n\n {leadingAdapter?.renderRowLeading?.({ node, runner })}\n\n <span\n className={cn(\n \"min-w-0 truncate\",\n selected ? \"font-semibold\" : \"font-medium\",\n (status && NAME_TONE[status]) || \"text-foreground\",\n )}\n title={message || undefined}\n >\n {humanizeName(node.name, node.framework)}\n </span>\n {/* The verdict trails the name on the same line and gives way first. */}\n {message ? (\n <span\n className=\"min-w-0 flex-1 shrink-[100] truncate text-xs text-muted-foreground\"\n title={message}\n data-row-message=\"\"\n >\n {message}\n </span>\n ) : (\n <span className=\"flex-1\" />\n )}\n\n {node.progress && node.progress.total > 0 && !node.passed && !node.failed && (\n <span className=\"flex shrink-0 items-center gap-1 text-xs text-blue-600 dark:text-blue-400\">\n {node.progress.phase && <span>{node.progress.phase}</span>}\n <span className=\"tabular-nums\">\n {node.progress.done}/{node.progress.total}\n </span>\n </span>\n )}\n\n {dur > 0 && (\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatTestDuration(dur)}</span>\n )}\n\n {hasChildren && <CountBadges node={node} />}\n\n {canStop && (\n <IconButton\n icon={UiStop}\n label=\"Stop\"\n disabled={runner.busy.stop}\n onClick={(e) => {\n e.stopPropagation();\n runner.onStop!(node);\n }}\n />\n )}\n\n {canRerun && (\n <IconButton\n icon={UiRefresh}\n label=\"Rerun\"\n disabled={runner.busy.rerun}\n onClick={(e) => {\n e.stopPropagation();\n runner.onRerun!(node);\n }}\n />\n )}\n </>\n );\n}\n"],"names":["sum","jsxs","jsx","Badge","useTestRunner","status","testStatus","outcomeFor","totalDuration","Fragment","OutcomeIcon","Icon","UiClass","ToolIcon","toolKeyFor","cn","humanizeName","formatTestDuration","IconButton","UiStop","UiRefresh"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TestTreeNode.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestTreeNode.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAyCpC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,+BA8FjF"}
1
+ {"version":3,"file":"TestTreeNode.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestTreeNode.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AA0CpC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,+BA8FjF"}