feat(FN-1437): theme workflow steps tab with CSS classes

- Replace inline styles in WorkflowResultsTab with semantic CSS classes
- Add .workflow-results-tab, .workflow-results-header, .workflow-step-card, .workflow-step-header, and .workflow-step-output CSS rules to styles.css
- Add tests for WorkflowResultsTab component with mocked workflow step data
This commit is contained in:
gsxdsm
2026-04-10 02:50:04 -07:00
parent d53cb83f02
commit 61adfc87de
3 changed files with 139 additions and 30 deletions

View File

@@ -96,21 +96,26 @@ describe("WorkflowResultsTab", () => {
// Passed badge
const passedBadge = screen.getByTestId("workflow-result-badge-WS-001");
expect(passedBadge).toHaveTextContent("Passed");
expect(passedBadge).toHaveStyle({ backgroundColor: "var(--color-success, #3fb950)" });
expect(passedBadge).toHaveClass("workflow-result-badge");
expect(passedBadge).toHaveClass("workflow-result-badge--passed");
// Failed badge
const failedBadge = screen.getByTestId("workflow-result-badge-WS-002");
expect(failedBadge).toHaveTextContent("Failed");
expect(failedBadge).toHaveStyle({ backgroundColor: "var(--color-error, #f85149)" });
expect(failedBadge).toHaveClass("workflow-result-badge");
expect(failedBadge).toHaveClass("workflow-result-badge--failed");
// Skipped badge
const skippedBadge = screen.getByTestId("workflow-result-badge-WS-003");
expect(skippedBadge).toHaveTextContent("Skipped");
expect(skippedBadge).toHaveClass("workflow-result-badge");
expect(skippedBadge).toHaveClass("workflow-result-badge--skipped");
// Pending badge
const pendingBadge = screen.getByTestId("workflow-result-badge-WS-004");
expect(pendingBadge).toHaveTextContent("Running…");
expect(pendingBadge).toHaveStyle({ backgroundColor: "var(--todo, #58a6ff)" });
expect(pendingBadge).toHaveClass("workflow-result-badge");
expect(pendingBadge).toHaveClass("workflow-result-badge--pending");
});
it("shows output content when toggle is clicked to expand", () => {
@@ -556,4 +561,94 @@ describe("WorkflowResultsTab", () => {
});
});
});
describe("theming contract", () => {
it("uses CSS classes for phase badges instead of inline styles", () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={[
{
workflowStepId: "WS-001",
workflowStepName: "Pre-merge Step",
phase: "pre-merge",
status: "passed",
},
{
workflowStepId: "WS-002",
workflowStepName: "Post-merge Step",
phase: "post-merge",
status: "passed",
},
]}
/>,
);
// Pre-merge phase badge should use CSS class
const preMergeBadge = screen.getByTestId("workflow-result-phase-WS-001");
expect(preMergeBadge).toHaveClass("phase-badge");
expect(preMergeBadge).toHaveClass("phase-badge--pre-merge");
// Check that there are no rgba() values in inline styles
expect(preMergeBadge.getAttribute("style") || "").not.toMatch(/rgba\(/);
// Post-merge phase badge should use CSS class
const postMergeBadge = screen.getByTestId("workflow-result-phase-WS-002");
expect(postMergeBadge).toHaveClass("phase-badge");
expect(postMergeBadge).toHaveClass("phase-badge--post-merge");
expect(postMergeBadge.getAttribute("style") || "").not.toMatch(/rgba\(/);
});
it("uses CSS classes for status badges instead of inline styles", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
const passedBadge = screen.getByTestId("workflow-result-badge-WS-001");
const failedBadge = screen.getByTestId("workflow-result-badge-WS-002");
const skippedBadge = screen.getByTestId("workflow-result-badge-WS-003");
const pendingBadge = screen.getByTestId("workflow-result-badge-WS-004");
// All badges should have CSS class-based styling
expect(passedBadge).toHaveClass("workflow-result-badge--passed");
expect(failedBadge).toHaveClass("workflow-result-badge--failed");
expect(skippedBadge).toHaveClass("workflow-result-badge--skipped");
expect(pendingBadge).toHaveClass("workflow-result-badge--pending");
// No inline background color styles with rgba values
const passedStyle = passedBadge.getAttribute("style") || "";
const failedStyle = failedBadge.getAttribute("style") || "";
expect(passedStyle).not.toMatch(/rgba\(/);
expect(failedStyle).not.toMatch(/rgba\(/);
});
it("prevents reintroduction of hardcoded phase colors in component source", () => {
// Read the component source file
const fs = require("fs");
const path = require("path");
const componentPath = path.join(__dirname, "WorkflowResultsTab.tsx");
const componentSource = fs.readFileSync(componentPath, "utf-8");
// These hardcoded color patterns should NOT appear in the component
// (they were the old inline style values)
const forbiddenPatterns = [
/rgba\(59,\s*130,\s*246,\s*0\.15\)/, // pre-merge background
/rgba\(139,\s*92,\s*246,\s*0\.15\)/, // post-merge background
/#[38]b82f6/, // pre-merge text (partial match for #3b82f6 or #8b5cf6)
/#[89]b5cf6/, // post-merge text (partial match for #8b5cf6)
];
for (const pattern of forbiddenPatterns) {
expect(componentSource).not.toMatch(pattern);
}
});
it("prevents reintroduction of getStatusColor function with hardcoded colors", () => {
const fs = require("fs");
const path = require("path");
const componentPath = path.join(__dirname, "WorkflowResultsTab.tsx");
const componentSource = fs.readFileSync(componentPath, "utf-8");
// The getStatusColor function should not exist (removed to use CSS classes)
expect(componentSource).not.toMatch(/function getStatusColor/);
expect(componentSource).not.toMatch(/getStatusColor\(/);
});
});
});

View File

@@ -21,21 +21,6 @@ interface WorkflowStepOption {
icon?: ReactNode;
}
function getStatusColor(status: WorkflowStepResult["status"]): string {
switch (status) {
case "passed":
return "var(--color-success, #3fb950)";
case "failed":
return "var(--color-error, #f85149)";
case "skipped":
return "var(--text-dim, #484f58)";
case "pending":
return "var(--todo, #58a6ff)";
default:
return "var(--text-dim, #484f58)";
}
}
function getStatusLabel(status: WorkflowStepResult["status"]): string {
switch (status) {
case "passed":
@@ -77,16 +62,10 @@ function getOutputPreview(output: string): string {
}
function phaseBadge(phase: "pre-merge" | "post-merge", id: string, prefix: string): ReactNode {
const phaseClass = phase === "post-merge" ? "phase-badge--post-merge" : "phase-badge--pre-merge";
return (
<span
style={{
marginLeft: "6px",
fontSize: "11px",
padding: "1px 6px",
borderRadius: "4px",
background: phase === "post-merge" ? "rgba(139, 92, 246, 0.15)" : "rgba(59, 130, 246, 0.15)",
color: phase === "post-merge" ? "#8b5cf6" : "#3b82f6",
}}
className={`phase-badge ${phaseClass}`}
data-testid={`${prefix}-${id}`}
>
{phase === "post-merge" ? "Post-merge" : "Pre-merge"}
@@ -352,10 +331,6 @@ export function WorkflowResultsTab({
</div>
<span
className={`workflow-result-badge workflow-result-badge--${result.status}`}
style={{
backgroundColor: getStatusColor(result.status),
color: result.status === "skipped" ? "var(--text-muted)" : "#fff",
}}
data-testid={`workflow-result-badge-${result.workflowStepId}`}
>
{getStatusLabel(result.status)}