story-kit: merge 236_story_show_test_results_for_a_story_in_expanded_work_item
This commit is contained in:
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import type { AgentEvent, AgentInfo, AgentStatusValue } from "../api/agents";
|
||||
import { agentsApi, subscribeAgentStream } from "../api/agents";
|
||||
import type { TestCaseResult, TestResultsResponse } from "../api/client";
|
||||
import { api } from "../api/client";
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
@@ -24,11 +25,89 @@ const STATUS_COLORS: Record<AgentStatusValue, string> = {
|
||||
|
||||
interface WorkItemDetailPanelProps {
|
||||
storyId: string;
|
||||
pipelineVersion: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function TestCaseRow({ tc }: { tc: TestCaseResult }) {
|
||||
const isPassing = tc.status === "pass";
|
||||
return (
|
||||
<div
|
||||
data-testid={`test-case-${tc.name}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
<span
|
||||
data-testid={`test-status-${tc.name}`}
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color: isPassing ? "#3fb950" : "#f85149",
|
||||
}}
|
||||
>
|
||||
{isPassing ? "PASS" : "FAIL"}
|
||||
</span>
|
||||
<span style={{ fontSize: "0.82em", color: "#ccc" }}>{tc.name}</span>
|
||||
</div>
|
||||
{tc.details && (
|
||||
<div
|
||||
data-testid={`test-details-${tc.name}`}
|
||||
style={{
|
||||
fontSize: "0.75em",
|
||||
color: "#888",
|
||||
paddingLeft: "22px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{tc.details}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TestSection({
|
||||
title,
|
||||
tests,
|
||||
testId,
|
||||
}: {
|
||||
title: string;
|
||||
tests: TestCaseResult[];
|
||||
testId: string;
|
||||
}) {
|
||||
const passCount = tests.filter((t) => t.status === "pass").length;
|
||||
const failCount = tests.length - passCount;
|
||||
return (
|
||||
<div data-testid={testId}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.78em",
|
||||
fontWeight: 600,
|
||||
color: "#aaa",
|
||||
marginBottom: "6px",
|
||||
}}
|
||||
>
|
||||
{title} ({passCount} passed, {failCount} failed)
|
||||
</div>
|
||||
{tests.length === 0 ? (
|
||||
<div style={{ fontSize: "0.75em", color: "#555", fontStyle: "italic" }}>
|
||||
No tests recorded
|
||||
</div>
|
||||
) : (
|
||||
tests.map((tc) => <TestCaseRow key={tc.name} tc={tc} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkItemDetailPanel({
|
||||
storyId,
|
||||
pipelineVersion,
|
||||
onClose,
|
||||
}: WorkItemDetailPanelProps) {
|
||||
const [content, setContent] = useState<string | null>(null);
|
||||
@@ -39,6 +118,9 @@ export function WorkItemDetailPanel({
|
||||
const [agentInfo, setAgentInfo] = useState<AgentInfo | null>(null);
|
||||
const [agentLog, setAgentLog] = useState<string[]>([]);
|
||||
const [agentStatus, setAgentStatus] = useState<AgentStatusValue | null>(null);
|
||||
const [testResults, setTestResults] = useState<TestResultsResponse | null>(
|
||||
null,
|
||||
);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
|
||||
@@ -60,6 +142,18 @@ export function WorkItemDetailPanel({
|
||||
});
|
||||
}, [storyId]);
|
||||
|
||||
// Fetch test results on mount and when pipeline updates arrive.
|
||||
useEffect(() => {
|
||||
api
|
||||
.getTestResults(storyId)
|
||||
.then((data) => {
|
||||
setTestResults(data);
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore — test results may not exist yet.
|
||||
});
|
||||
}, [storyId, pipelineVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
cleanupRef.current?.();
|
||||
cleanupRef.current = null;
|
||||
@@ -126,6 +220,9 @@ export function WorkItemDetailPanel({
|
||||
}, [onClose]);
|
||||
|
||||
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
||||
const hasTestResults =
|
||||
testResults &&
|
||||
(testResults.unit.length > 0 || testResults.integration.length > 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -255,6 +352,56 @@ export function WorkItemDetailPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test Results section */}
|
||||
<div
|
||||
data-testid="test-results-section"
|
||||
style={{
|
||||
border: "1px solid #2a2a2a",
|
||||
borderRadius: "8px",
|
||||
padding: "10px 12px",
|
||||
background: "#161616",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.8em",
|
||||
color: "#555",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
Test Results
|
||||
</div>
|
||||
{hasTestResults ? (
|
||||
<div
|
||||
data-testid="test-results-content"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
<TestSection
|
||||
title="Unit Tests"
|
||||
tests={testResults.unit}
|
||||
testId="test-section-unit"
|
||||
/>
|
||||
<TestSection
|
||||
title="Integration Tests"
|
||||
tests={testResults.integration}
|
||||
testId="test-section-integration"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
data-testid="test-results-empty"
|
||||
style={{ fontSize: "0.75em", color: "#444" }}
|
||||
>
|
||||
No test results recorded
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -336,7 +483,6 @@ export function WorkItemDetailPanel({
|
||||
{/* Placeholder sections for future content */}
|
||||
{(
|
||||
[
|
||||
{ id: "test-output", label: "Test Output" },
|
||||
{ id: "coverage", label: "Coverage" },
|
||||
] as { id: string; label: string }[]
|
||||
).map(({ id, label }) => (
|
||||
|
||||
Reference in New Issue
Block a user