feat(FN-1030): enhance workflow results tab with summary bar, collapsible output, and always-visible tab
- Make Workflow tab always visible in TaskDetailModal (not conditional on enabled steps) - Add summary bar showing pass/fail/skip counts with color-coded badges - Make workflow step output collapsible with expand/collapse toggle - Add enhanced empty states for no steps configured, all skipped, and no results - Add CSS styles for summary bar, collapsible output, and empty state variants - Update TaskDetailModal tests to reflect always-visible Workflow tab - Add comprehensive tests for WorkflowResultsTab new features
This commit is contained in:
@@ -272,9 +272,8 @@ export function TaskDetailModal({
|
||||
}, [projectId]);
|
||||
|
||||
// Load workflow results when workflow tab is active
|
||||
const hasWorkflowSteps = (task.enabledWorkflowSteps?.length ?? 0) > 0 || (task.workflowStepResults?.length ?? 0) > 0;
|
||||
useEffect(() => {
|
||||
if (activeTab !== "workflow" || !hasWorkflowSteps) return;
|
||||
if (activeTab !== "workflow") return;
|
||||
let cancelled = false;
|
||||
setWorkflowResultsLoading(true);
|
||||
fetchWorkflowResults(task.id, projectId)
|
||||
@@ -290,7 +289,7 @@ export function TaskDetailModal({
|
||||
if (!cancelled) setWorkflowResultsLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [activeTab, task.id, projectId, hasWorkflowSteps, addToast]);
|
||||
}, [activeTab, task.id, projectId, addToast]);
|
||||
|
||||
// Reset dependency search when dropdown closes
|
||||
useEffect(() => {
|
||||
@@ -862,18 +861,21 @@ export function TaskDetailModal({
|
||||
>
|
||||
Model
|
||||
</button>
|
||||
{hasWorkflowSteps && (
|
||||
<button
|
||||
className={`detail-tab${activeTab === "workflow" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("workflow")}
|
||||
>
|
||||
Workflow
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`detail-tab${activeTab === "workflow" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("workflow")}
|
||||
>
|
||||
Workflow
|
||||
</button>
|
||||
</div>
|
||||
{activeTab === "workflow" ? (
|
||||
<div className="detail-section">
|
||||
<WorkflowResultsTab taskId={task.id} results={workflowResults} loading={workflowResultsLoading} />
|
||||
<WorkflowResultsTab
|
||||
taskId={task.id}
|
||||
results={workflowResults}
|
||||
loading={workflowResultsLoading}
|
||||
enabledWorkflowSteps={task.enabledWorkflowSteps}
|
||||
/>
|
||||
</div>
|
||||
) : activeTab === "model" ? (
|
||||
<div className="detail-section">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { WorkflowResultsTab } from "./WorkflowResultsTab";
|
||||
import type { WorkflowStepResult } from "@fusion/core";
|
||||
|
||||
@@ -76,30 +76,73 @@ describe("WorkflowResultsTab", () => {
|
||||
expect(pendingBadge).toHaveStyle({ backgroundColor: "var(--todo, #58a6ff)" });
|
||||
});
|
||||
|
||||
it("shows output content for each result", () => {
|
||||
it("shows output content when toggle is clicked to expand", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
// Output should be hidden by default (collapsed)
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-001")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-002")).not.toBeInTheDocument();
|
||||
|
||||
// Click "Show output" for WS-001
|
||||
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
|
||||
|
||||
// Now output should be visible
|
||||
expect(screen.getByTestId("workflow-result-output-WS-001")).toHaveTextContent(
|
||||
"All tests passed successfully."
|
||||
);
|
||||
expect(screen.getByTestId("workflow-result-output-WS-002")).toHaveTextContent(
|
||||
"Found 2 security issues in auth.ts"
|
||||
);
|
||||
|
||||
// WS-002 should still be collapsed
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-002")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides output when toggle is clicked again", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
// Expand WS-001
|
||||
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
|
||||
expect(screen.getByTestId("workflow-result-output-WS-001")).toBeInTheDocument();
|
||||
|
||||
// Toggle text should say "Hide output"
|
||||
expect(screen.getByTestId("workflow-result-toggle-WS-001")).toHaveTextContent("Hide output");
|
||||
|
||||
// Collapse WS-001
|
||||
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
|
||||
|
||||
// Output should be hidden again
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-001")).not.toBeInTheDocument();
|
||||
|
||||
// Toggle text should say "Show output"
|
||||
expect(screen.getByTestId("workflow-result-toggle-WS-001")).toHaveTextContent("Show output");
|
||||
});
|
||||
|
||||
it("handles results without output gracefully", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
// WS-003 and WS-004 have no output, so output elements should not be rendered
|
||||
// WS-003 and WS-004 have no output, so output section elements should not be rendered
|
||||
expect(screen.queryByTestId("workflow-result-toggle-WS-003")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("workflow-result-toggle-WS-004")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-003")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-004")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty state when no results", () => {
|
||||
it("shows empty state when no workflow steps are configured", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={[]} />);
|
||||
|
||||
expect(screen.getByTestId("workflow-results-empty")).toBeInTheDocument();
|
||||
expect(screen.getByText("No workflow steps have run yet.")).toBeInTheDocument();
|
||||
expect(screen.getByText("No workflow steps configured for this task.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'configured but not run' empty state when enabledWorkflowSteps is non-empty", () => {
|
||||
render(
|
||||
<WorkflowResultsTab
|
||||
taskId="FN-001"
|
||||
results={[]}
|
||||
enabledWorkflowSteps={["WS-001", "WS-002"]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("workflow-results-empty")).toBeInTheDocument();
|
||||
expect(screen.getByText("Workflow steps configured but haven't run yet.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows loading state when loading prop is true", () => {
|
||||
@@ -167,4 +210,118 @@ describe("WorkflowResultsTab", () => {
|
||||
|
||||
expect(screen.getByTestId("workflow-result-phase-WS-005")).toHaveTextContent("Pre-merge");
|
||||
});
|
||||
|
||||
describe("summary bar", () => {
|
||||
it("renders summary bar with correct counts", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
const summary = screen.getByTestId("workflow-results-summary");
|
||||
expect(summary).toBeInTheDocument();
|
||||
expect(summary).toHaveTextContent("4 steps");
|
||||
expect(summary).toHaveTextContent("1 passed");
|
||||
expect(summary).toHaveTextContent("1 failed");
|
||||
expect(summary).toHaveTextContent("1 skipped");
|
||||
expect(summary).toHaveTextContent("1 running");
|
||||
});
|
||||
|
||||
it("shows plural 'step' for single result", () => {
|
||||
const singleResult: WorkflowStepResult[] = [
|
||||
{
|
||||
workflowStepId: "WS-001",
|
||||
workflowStepName: "QA Check",
|
||||
status: "passed",
|
||||
output: "Done",
|
||||
},
|
||||
];
|
||||
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={singleResult} />);
|
||||
|
||||
const summary = screen.getByTestId("workflow-results-summary");
|
||||
expect(summary).toHaveTextContent("1 step");
|
||||
expect(summary).toHaveTextContent("1 passed");
|
||||
// Should not include "0 failed" etc. for zero-count categories
|
||||
expect(summary).not.toHaveTextContent("0 failed");
|
||||
});
|
||||
|
||||
it("omits zero-count categories from summary", () => {
|
||||
const allPassed: WorkflowStepResult[] = [
|
||||
{ workflowStepId: "WS-001", workflowStepName: "Check 1", status: "passed" },
|
||||
{ workflowStepId: "WS-002", workflowStepName: "Check 2", status: "passed" },
|
||||
];
|
||||
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={allPassed} />);
|
||||
|
||||
const summary = screen.getByTestId("workflow-results-summary");
|
||||
expect(summary).toHaveTextContent("2 steps");
|
||||
expect(summary).toHaveTextContent("2 passed");
|
||||
expect(summary).not.toHaveTextContent("failed");
|
||||
expect(summary).not.toHaveTextContent("skipped");
|
||||
expect(summary).not.toHaveTextContent("running");
|
||||
});
|
||||
});
|
||||
|
||||
describe("collapsible output", () => {
|
||||
it("output sections default to collapsed", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
// Outputs should not be rendered in DOM by default
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-001")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-002")).not.toBeInTheDocument();
|
||||
|
||||
// Toggles should say "Show output"
|
||||
expect(screen.getByTestId("workflow-result-toggle-WS-001")).toHaveTextContent("Show output");
|
||||
expect(screen.getByTestId("workflow-result-toggle-WS-002")).toHaveTextContent("Show output");
|
||||
});
|
||||
|
||||
it("shows preview hint when output is collapsed", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
// Preview should show for results with output
|
||||
expect(screen.getByTestId("workflow-result-preview-WS-001")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("workflow-result-preview-WS-002")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows line count in preview for multi-line output", () => {
|
||||
const multiLineResult: WorkflowStepResult[] = [
|
||||
{
|
||||
workflowStepId: "WS-010",
|
||||
workflowStepName: "Multi Line Check",
|
||||
status: "passed",
|
||||
output: "Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
|
||||
},
|
||||
];
|
||||
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={multiLineResult} />);
|
||||
|
||||
expect(screen.getByTestId("workflow-result-preview-WS-010")).toHaveTextContent("5 lines");
|
||||
});
|
||||
|
||||
it("shows output text as preview for single-line output", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
// WS-001 output is "All tests passed successfully." — single line
|
||||
expect(screen.getByTestId("workflow-result-preview-WS-001")).toHaveTextContent(
|
||||
"All tests passed successfully."
|
||||
);
|
||||
});
|
||||
|
||||
it("expands and collapses independently per step", () => {
|
||||
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
|
||||
|
||||
// Expand WS-001
|
||||
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
|
||||
expect(screen.getByTestId("workflow-result-output-WS-001")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-002")).not.toBeInTheDocument();
|
||||
|
||||
// Expand WS-002 as well
|
||||
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-002"));
|
||||
expect(screen.getByTestId("workflow-result-output-WS-001")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("workflow-result-output-WS-002")).toBeInTheDocument();
|
||||
|
||||
// Collapse WS-001
|
||||
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
|
||||
expect(screen.queryByTestId("workflow-result-output-WS-001")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("workflow-result-output-WS-002")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import type { WorkflowStepResult } from "@fusion/core";
|
||||
|
||||
interface WorkflowResultsTabProps {
|
||||
taskId: string;
|
||||
results: WorkflowStepResult[];
|
||||
loading?: boolean;
|
||||
enabledWorkflowSteps?: string[];
|
||||
}
|
||||
|
||||
function getStatusColor(status: WorkflowStepResult["status"]): string {
|
||||
@@ -55,7 +57,19 @@ function formatTimestamp(iso?: string): string | null {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
export function WorkflowResultsTab({ taskId, results, loading }: WorkflowResultsTabProps) {
|
||||
function getOutputPreview(output: string): string {
|
||||
const lines = output.split("\n");
|
||||
if (lines.length <= 1) return output;
|
||||
return `${lines.length} lines`;
|
||||
}
|
||||
|
||||
export function WorkflowResultsTab({ taskId, results, loading, enabledWorkflowSteps }: WorkflowResultsTabProps) {
|
||||
const [expandedOutputs, setExpandedOutputs] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleOutput = (stepId: string) => {
|
||||
setExpandedOutputs((prev) => ({ ...prev, [stepId]: !prev[stepId] }));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="workflow-results-loading" data-testid="workflow-results-loading">
|
||||
@@ -66,9 +80,14 @@ export function WorkflowResultsTab({ taskId, results, loading }: WorkflowResults
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
const hasConfiguredSteps = (enabledWorkflowSteps?.length ?? 0) > 0;
|
||||
return (
|
||||
<div className="workflow-results-empty" data-testid="workflow-results-empty">
|
||||
<p>No workflow steps have run yet.</p>
|
||||
<p>
|
||||
{hasConfiguredSteps
|
||||
? "Workflow steps configured but haven't run yet."
|
||||
: "No workflow steps configured for this task."}
|
||||
</p>
|
||||
<p className="workflow-results-empty-hint">
|
||||
Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.
|
||||
</p>
|
||||
@@ -76,10 +95,26 @@ export function WorkflowResultsTab({ taskId, results, loading }: WorkflowResults
|
||||
);
|
||||
}
|
||||
|
||||
// Compute summary counts
|
||||
const passed = results.filter((r) => r.status === "passed").length;
|
||||
const failed = results.filter((r) => r.status === "failed").length;
|
||||
const skipped = results.filter((r) => r.status === "skipped").length;
|
||||
const pending = results.filter((r) => r.status === "pending").length;
|
||||
|
||||
const summaryParts: string[] = [`${results.length} step${results.length !== 1 ? "s" : ""}`];
|
||||
if (passed > 0) summaryParts.push(`${passed} passed`);
|
||||
if (failed > 0) summaryParts.push(`${failed} failed`);
|
||||
if (skipped > 0) summaryParts.push(`${skipped} skipped`);
|
||||
if (pending > 0) summaryParts.push(`${pending} running`);
|
||||
|
||||
return (
|
||||
<div className="workflow-results-list" data-testid="workflow-results-list">
|
||||
<div className="workflow-results-summary-bar" data-testid="workflow-results-summary">
|
||||
{summaryParts.join(" · ")}
|
||||
</div>
|
||||
{results.map((result, index) => {
|
||||
const phase = result.phase || "pre-merge";
|
||||
const isExpanded = expandedOutputs[result.workflowStepId] ?? false;
|
||||
return (
|
||||
<div
|
||||
key={`${result.workflowStepId}-${index}`}
|
||||
@@ -135,13 +170,29 @@ export function WorkflowResultsTab({ taskId, results, loading }: WorkflowResults
|
||||
|
||||
{result.output && (
|
||||
<div className="workflow-result-output-section">
|
||||
<div className="workflow-result-output-label">Output:</div>
|
||||
<pre
|
||||
className="workflow-result-output"
|
||||
data-testid={`workflow-result-output-${result.workflowStepId}`}
|
||||
>
|
||||
{result.output}
|
||||
</pre>
|
||||
<div className="workflow-result-output-header">
|
||||
<span className="workflow-result-output-label">Output:</span>
|
||||
<button
|
||||
className="workflow-result-toggle"
|
||||
onClick={() => toggleOutput(result.workflowStepId)}
|
||||
data-testid={`workflow-result-toggle-${result.workflowStepId}`}
|
||||
>
|
||||
{isExpanded ? "Hide output" : "Show output"}
|
||||
</button>
|
||||
{!isExpanded && (
|
||||
<span className="workflow-result-output-preview" data-testid={`workflow-result-preview-${result.workflowStepId}`}>
|
||||
{getOutputPreview(result.output)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<pre
|
||||
className="workflow-result-output"
|
||||
data-testid={`workflow-result-output-${result.workflowStepId}`}
|
||||
>
|
||||
{result.output}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -917,22 +917,23 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
// For an in-progress task (no workflow steps, no merge commit),
|
||||
// the top-level tabs are: Definition, Logs, Changes, Comments, Model
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Model"];
|
||||
// the top-level tabs are: Definition, Logs, Changes, Comments, Model, Workflow
|
||||
const tabTexts = ["Definition", "Logs", "Changes", "Comments", "Model", "Workflow"];
|
||||
const tabs = screen.getAllByRole("button").filter((b) =>
|
||||
tabTexts.includes(b.textContent || "")
|
||||
);
|
||||
expect(tabs.length).toBe(5);
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
expect(tabs[5].textContent).toBe("Workflow");
|
||||
|
||||
// Activity and Agent Log are NOT top-level tabs (they are subviews inside Logs)
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(5);
|
||||
// Workflow tab should NOT appear when no workflow steps are configured
|
||||
expect(screen.queryByText("Workflow")).toBeNull();
|
||||
expect(container.querySelectorAll(".detail-tab").length).toBe(6);
|
||||
// Workflow tab should always appear even when no workflow steps are configured
|
||||
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
||||
// Commits tab should NOT appear for non-done tasks
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
@@ -1776,7 +1777,7 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(5); // Definition, Logs, Changes, Comments, Model
|
||||
expect(tabs.length).toBe(6); // Definition, Logs, Changes, Comments, Model, Workflow
|
||||
// Tabs should use class-based styling, not inline styles
|
||||
expect(tabs[0].classList.contains("detail-tab")).toBe(true);
|
||||
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true); // Definition is default active
|
||||
@@ -1784,6 +1785,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[2].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[3].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[4].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[5].classList.contains("detail-tab-active")).toBe(false);
|
||||
// Verify no inline padding/fontSize (responsive CSS controls this)
|
||||
expect((tabs[0] as HTMLElement).style.padding).toBe("");
|
||||
expect((tabs[0] as HTMLElement).style.fontSize).toBe("");
|
||||
@@ -2274,17 +2276,17 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// In-progress tasks without workflow steps show exactly 5 tabs:
|
||||
// Definition, Logs, Changes, Comments, Model
|
||||
// In-progress tasks show exactly 6 tabs:
|
||||
// Definition, Logs, Changes, Comments, Model, Workflow
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(5);
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
// Conditional tabs should NOT be present
|
||||
expect(screen.queryByText("Workflow")).toBeNull();
|
||||
expect(tabs[5].textContent).toBe("Workflow");
|
||||
// Commits tab should NOT be present for non-done tasks
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -2328,15 +2330,16 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Commits, Comments, Model
|
||||
// Done task with commit SHA: Definition, Logs, Changes, Commits, Comments, Model, Workflow
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Logs");
|
||||
expect(tabs[2].textContent).toBe("Changes");
|
||||
expect(tabs[3].textContent).toBe("Commits");
|
||||
expect(tabs[4].textContent).toBe("Comments");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
expect(tabs[6].textContent).toBe("Workflow");
|
||||
});
|
||||
|
||||
it("shows all conditional tabs for done task with workflow steps and commit SHA", () => {
|
||||
@@ -2383,9 +2386,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
|
||||
expect(triageTabs.length).toBe(4); // Definition, Logs, Comments, Model
|
||||
expect(triageTabs.length).toBe(5); // Definition, Logs, Comments, Model, Workflow
|
||||
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Model",
|
||||
"Definition", "Logs", "Comments", "Model", "Workflow",
|
||||
]);
|
||||
|
||||
const { container: todoContainer } = render(
|
||||
@@ -2401,9 +2404,9 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
|
||||
expect(todoTabs.length).toBe(4); // Definition, Logs, Comments, Model
|
||||
expect(todoTabs.length).toBe(5); // Definition, Logs, Comments, Model, Workflow
|
||||
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
|
||||
"Definition", "Logs", "Comments", "Model",
|
||||
"Definition", "Logs", "Comments", "Model", "Workflow",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -3928,7 +3931,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
describe("Workflow tab", () => {
|
||||
it("does NOT show Workflow tab when enabledWorkflowSteps is empty", () => {
|
||||
it("shows Workflow tab even when enabledWorkflowSteps is empty", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ enabledWorkflowSteps: [] })}
|
||||
@@ -3941,10 +3944,10 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Workflow")).toBeNull();
|
||||
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does NOT show Workflow tab when enabledWorkflowSteps is undefined", () => {
|
||||
it("shows Workflow tab even when enabledWorkflowSteps is undefined", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ enabledWorkflowSteps: undefined, workflowStepResults: undefined })}
|
||||
@@ -3957,7 +3960,7 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Workflow")).toBeNull();
|
||||
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Workflow tab when enabledWorkflowSteps is non-empty", () => {
|
||||
@@ -4108,7 +4111,7 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-results-empty")).toBeTruthy();
|
||||
expect(screen.getByText("No workflow steps have run yet.")).toBeTruthy();
|
||||
expect(screen.getByText("Workflow steps configured but haven't run yet.")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19196,6 +19196,45 @@ html .column.drag-over * {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.workflow-results-summary-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs, 4px);
|
||||
padding: var(--space-sm, 8px) var(--space-md, 16px);
|
||||
font-size: 13px;
|
||||
color: var(--text-muted, #8b949e);
|
||||
border: 1px solid var(--border, #30363d);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-elevated, #161b22);
|
||||
}
|
||||
|
||||
.workflow-result-output-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm, 8px);
|
||||
margin-bottom: var(--space-xs, 4px);
|
||||
}
|
||||
|
||||
.workflow-result-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--todo, #58a6ff);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.workflow-result-toggle:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.workflow-result-output-preview {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim, #484f58);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ===== Model Onboarding Modal ===== */
|
||||
|
||||
.model-onboarding-modal {
|
||||
|
||||
Reference in New Issue
Block a user