Files
fusion/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx
Fusion 2811f2c30e test(FN-4246): cover safe-area top padding in mobile modal CSS
Fusion-Task-Id: FN-4246
Fusion-Task-Lineage: 40ca0357-149d-423c-a9c0-79d2b0d77a13
2026-05-13 09:06:20 -07:00

1130 lines
44 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import { WorkflowResultsTab } from "../WorkflowResultsTab";
import { fetchWorkflowSteps } from "../../api";
import { useAgentLogs } from "../../hooks/useAgentLogs";
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
import type { AgentLogEntry, WorkflowStep, WorkflowStepResult } from "@fusion/core";
vi.mock("../../api", () => ({
fetchWorkflowSteps: vi.fn(),
}));
vi.mock("../../hooks/useAgentLogs", () => ({
useAgentLogs: vi.fn(),
}));
const mockedFetchWorkflowSteps = vi.mocked(fetchWorkflowSteps);
const mockedUseAgentLogs = vi.mocked(useAgentLogs);
describe("WorkflowResultsTab", () => {
const mockWorkflowSteps: WorkflowStep[] = [
{
id: "WS-101",
name: "QA Check",
description: "Run test suite",
mode: "prompt",
phase: "pre-merge",
prompt: "Run QA checks",
enabled: true,
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
},
{
id: "WS-102",
name: "Docs Review",
description: "Review docs",
mode: "prompt",
phase: "post-merge",
prompt: "Review docs",
enabled: true,
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
},
{
id: "WS-103",
name: "Browser Verification",
description: "Verify web application functionality using browser automation",
mode: "prompt",
phase: "pre-merge",
prompt: "Verify browser flows",
enabled: true,
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
templateId: "browser-verification",
},
];
beforeEach(() => {
mockedFetchWorkflowSteps.mockReset();
mockedFetchWorkflowSteps.mockResolvedValue(mockWorkflowSteps);
mockedUseAgentLogs.mockReset();
mockedUseAgentLogs.mockReturnValue({
entries: [],
loading: false,
clear: vi.fn(),
loadMore: vi.fn(),
hasMore: false,
total: 0,
loadingMore: false,
});
});
const mockResults: WorkflowStepResult[] = [
{
workflowStepId: "WS-001",
workflowStepName: "QA Check",
phase: "pre-merge",
status: "passed",
output: "All tests passed successfully.",
startedAt: "2026-03-31T10:00:00Z",
completedAt: "2026-03-31T10:02:30Z",
},
{
workflowStepId: "WS-002",
workflowStepName: "Security Audit",
phase: "pre-merge",
status: "failed",
output: "Found 2 security issues in auth.ts",
startedAt: "2026-03-31T10:02:35Z",
completedAt: "2026-03-31T10:03:15Z",
},
{
workflowStepId: "WS-003",
workflowStepName: "Documentation Review",
phase: "post-merge",
status: "skipped",
output: undefined,
startedAt: undefined,
completedAt: undefined,
},
{
workflowStepId: "WS-004",
workflowStepName: "Performance Check",
phase: "post-merge",
status: "pending",
output: undefined,
startedAt: "2026-03-31T10:03:20Z",
completedAt: undefined,
},
];
it("renders list of workflow step results", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
expect(screen.getByTestId("workflow-results-list")).toBeInTheDocument();
expect(screen.getByText("QA Check")).toBeInTheDocument();
expect(screen.getByText("Security Audit")).toBeInTheDocument();
expect(screen.getByText("Documentation Review")).toBeInTheDocument();
expect(screen.getByText("Performance Check")).toBeInTheDocument();
});
it("renders correct status badges for each result", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Passed badge
const passedBadge = screen.getByTestId("workflow-result-badge-WS-001");
expect(passedBadge).toHaveTextContent("Passed");
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).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).toHaveClass("workflow-result-badge");
expect(pendingBadge).toHaveClass("workflow-result-badge--pending");
});
it("FN-4214: shows waiting placeholder when pending-step entries are all stale", () => {
const historicalEntries: AgentLogEntry[] = [
{
timestamp: "2026-03-31T10:03:00Z",
taskId: "FN-001",
text: "Earlier workflow output",
type: "text",
},
];
mockedUseAgentLogs.mockReturnValue({
entries: historicalEntries,
loading: false,
clear: vi.fn(),
loadMore: vi.fn(),
hasMore: false,
total: historicalEntries.length,
loadingMore: false,
});
render(
<WorkflowResultsTab taskId="FN-001" results={mockResults} isTaskInProgress />,
);
const liveLogPanel = screen.getByTestId("workflow-live-log-WS-004");
expect(within(liveLogPanel).getByText("Waiting for agent output…")).toBeInTheDocument();
expect(screen.queryByText("Earlier workflow output")).not.toBeInTheDocument();
});
it("FN-4214: hides waiting placeholder when current-step log entries exist", () => {
const currentStepEntries: AgentLogEntry[] = [
{
timestamp: "2026-03-31T10:03:25Z",
taskId: "FN-001",
text: "Current workflow output",
type: "text",
},
];
mockedUseAgentLogs.mockReturnValue({
entries: currentStepEntries,
loading: false,
clear: vi.fn(),
loadMore: vi.fn(),
hasMore: false,
total: currentStepEntries.length,
loadingMore: false,
});
render(
<WorkflowResultsTab taskId="FN-001" results={mockResults} isTaskInProgress />,
);
const liveLogPanel = screen.getByTestId("workflow-live-log-WS-004");
expect(within(liveLogPanel).queryByText("Waiting for agent output…")).not.toBeInTheDocument();
expect(within(liveLogPanel).getByText("Current workflow output")).toBeInTheDocument();
});
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."
);
// 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 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 workflow steps are configured", () => {
render(<WorkflowResultsTab taskId="FN-001" results={[]} />);
expect(screen.getByTestId("workflow-results-empty")).toBeInTheDocument();
expect(screen.getByText("No workflow steps configured for this task.")).toBeInTheDocument();
});
it("shows configured step details when enabledWorkflowSteps is non-empty and results are empty", async () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
enabledWorkflowSteps={["WS-101", "WS-102"]}
/>,
);
expect(screen.getByTestId("workflow-configured-steps")).toBeInTheDocument();
expect(screen.getByTestId("workflow-configured-header")).toBeInTheDocument();
expect(screen.getByTestId("workflow-configured-count")).toHaveTextContent("2 steps");
const qaStep = await screen.findByTestId("workflow-configured-step-WS-101");
const docsStep = await screen.findByTestId("workflow-configured-step-WS-102");
expect(qaStep).toHaveTextContent("QA Check");
expect(qaStep).toHaveTextContent("Run test suite");
expect(screen.getByTestId("workflow-configured-phase-WS-101")).toHaveTextContent("Pre-merge");
expect(docsStep).toHaveTextContent("Docs Review");
expect(docsStep).toHaveTextContent("Review docs");
expect(screen.getByTestId("workflow-configured-phase-WS-102")).toHaveTextContent("Post-merge");
expect(screen.getByText("Pre-merge steps run after implementation, before merge. Post-merge steps run after merge succeeds.")).toBeInTheDocument();
});
it("falls back to step ID and default description when definition is missing", () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
enabledWorkflowSteps={["WS-unknown"]}
/>,
);
const fallbackStep = screen.getByTestId("workflow-configured-step-WS-unknown");
expect(fallbackStep).toHaveTextContent("WS-unknown");
expect(fallbackStep).toHaveTextContent("Step definition not found.");
expect(screen.getByTestId("workflow-configured-phase-WS-unknown")).toHaveTextContent("Pre-merge");
});
it("shows loading state when loading prop is true", () => {
render(<WorkflowResultsTab taskId="FN-001" results={[]} loading={true} />);
expect(screen.getByTestId("workflow-results-loading")).toBeInTheDocument();
expect(screen.getByText("Loading workflow results…")).toBeInTheDocument();
});
it("displays execution timestamps when available", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Check that timestamps are displayed for results that have them
const timestamps = screen.getAllByText(/Started:/);
expect(timestamps.length).toBeGreaterThanOrEqual(3); // 3 results have startedAt
});
it("displays duration when start and end times are available", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// The first result has a 2m 30s duration
expect(screen.getByText("2m 30s")).toBeInTheDocument();
});
it("handles results with missing timestamps gracefully", () => {
const resultsWithoutTimestamps: WorkflowStepResult[] = [
{
workflowStepId: "WS-005",
workflowStepName: "Simple Check",
phase: "pre-merge",
status: "passed",
output: "Done",
},
];
render(<WorkflowResultsTab taskId="FN-001" results={resultsWithoutTimestamps} />);
expect(screen.getByText("Simple Check")).toBeInTheDocument();
// Should not crash without timestamps
});
it("displays phase badges for each result", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Pre-merge results (WS-001, WS-002)
expect(screen.getByTestId("workflow-result-phase-WS-001")).toHaveTextContent("Pre-merge");
expect(screen.getByTestId("workflow-result-phase-WS-002")).toHaveTextContent("Pre-merge");
// Post-merge results (WS-003, WS-004)
expect(screen.getByTestId("workflow-result-phase-WS-003")).toHaveTextContent("Post-merge");
expect(screen.getByTestId("workflow-result-phase-WS-004")).toHaveTextContent("Post-merge");
});
it("defaults to Pre-merge phase badge when phase is undefined", () => {
const resultsWithoutPhase: WorkflowStepResult[] = [
{
workflowStepId: "WS-005",
workflowStepName: "Legacy Check",
status: "passed",
output: "Done",
},
];
render(<WorkflowResultsTab taskId="FN-001" results={resultsWithoutPhase} />);
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();
});
});
describe("markdown rendering toggle", () => {
it("shows markdown mode toggle button when output is expanded", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand WS-001
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
// Mode toggle should be visible
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toBeInTheDocument();
});
it("defaults to markdown mode", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand WS-001
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
// Mode toggle should show "Markdown" (current mode)
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Markdown");
});
it("toggles between markdown and plain mode", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand WS-001
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
// Should start in markdown mode
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Markdown");
// Toggle to plain mode
fireEvent.click(screen.getByTestId("workflow-result-mode-toggle-WS-001"));
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Plain");
// Toggle back to markdown mode
fireEvent.click(screen.getByTestId("workflow-result-mode-toggle-WS-001"));
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Markdown");
});
it("mode toggle is independent per step", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand both WS-001 and WS-002
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-002"));
// Both should default to markdown mode
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Markdown");
expect(screen.getByTestId("workflow-result-mode-toggle-WS-002")).toHaveTextContent("Markdown");
// Toggle WS-001 to plain mode
fireEvent.click(screen.getByTestId("workflow-result-mode-toggle-WS-001"));
// WS-001 should be plain, WS-002 should still be markdown
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Plain");
expect(screen.getByTestId("workflow-result-mode-toggle-WS-002")).toHaveTextContent("Markdown");
});
it("FN-4209: header wraps when preview content is long", () => {
const longToken = "X".repeat(520);
const longOutputResults: WorkflowStepResult[] = [
{
...mockResults[0],
output: `Preview ${longToken}`,
},
];
render(<WorkflowResultsTab taskId="FN-001" results={longOutputResults} />);
const outputHeader = document.querySelector(".workflow-result-output-header");
expect(outputHeader).not.toBeNull();
if (!outputHeader) {
return;
}
expect(getComputedStyle(outputHeader).flexWrap).toBe("wrap");
const preview = outputHeader.querySelector(".workflow-result-output-preview");
expect(preview).not.toBeNull();
const outputToggle = screen.getByTestId("workflow-result-toggle-WS-001");
expect(outputToggle).toBeInTheDocument();
});
it("does not show mode toggle when output is collapsed", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Mode toggle should not be visible when collapsed
expect(screen.queryByTestId("workflow-result-mode-toggle-WS-001")).not.toBeInTheDocument();
});
it("renders markdown content when in markdown mode", () => {
const markdownResult: WorkflowStepResult[] = [
{
workflowStepId: "WS-MD",
workflowStepName: "Markdown Check",
status: "passed",
output: "# Header\n\n- Item 1\n- Item 2",
},
];
render(<WorkflowResultsTab taskId="FN-001" results={markdownResult} />);
// Expand
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-MD"));
// Should be in markdown mode (default)
expect(screen.getByTestId("workflow-result-mode-toggle-WS-MD")).toHaveTextContent("Markdown");
// Check that the output container has markdown-body class
const outputContainer = screen.getByTestId("workflow-result-output-WS-MD");
expect(outputContainer).toHaveClass("workflow-result-output--markdown");
});
it("renders plain text when in plain mode", () => {
const markdownResult: WorkflowStepResult[] = [
{
workflowStepId: "WS-MD",
workflowStepName: "Markdown Check",
status: "passed",
output: "# Header\n\n- Item 1\n- Item 2",
},
];
render(<WorkflowResultsTab taskId="FN-001" results={markdownResult} />);
// Expand
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-MD"));
// Toggle to plain mode
fireEvent.click(screen.getByTestId("workflow-result-mode-toggle-WS-MD"));
// Output container should not have markdown class
const outputContainer = screen.getByTestId("workflow-result-output-WS-MD");
expect(outputContainer).not.toHaveClass("workflow-result-output--markdown");
// Should show the raw markdown as preformatted text
expect(outputContainer.textContent).toContain("# Header");
});
});
describe("workflow step editing", () => {
it("shows edit button when canEdit is true and configured steps are present", () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-101"]}
/>,
);
expect(screen.getByTestId("workflow-steps-edit-toggle")).toBeInTheDocument();
});
it("does not show edit button when canEdit is false or undefined", () => {
const { rerender } = render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit={false}
enabledWorkflowSteps={["WS-101"]}
/>,
);
expect(screen.queryByTestId("workflow-steps-edit-toggle")).not.toBeInTheDocument();
rerender(<WorkflowResultsTab taskId="FN-001" results={[]} enabledWorkflowSteps={["WS-101"]} />);
expect(screen.queryByTestId("workflow-steps-edit-toggle")).not.toBeInTheDocument();
});
it("shows and hides workflow step checkboxes when edit is toggled", async () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-101"]}
/>,
);
expect(screen.queryByTestId("workflow-steps-editor")).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
expect(screen.getByTestId("workflow-steps-editor")).toBeInTheDocument();
await screen.findByTestId("workflow-step-checkbox-WS-101");
expect(screen.getByTestId("workflow-step-checkbox-WS-103")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
expect(screen.queryByTestId("workflow-steps-editor")).not.toBeInTheDocument();
});
it("calls onWorkflowStepsChange when checking and unchecking steps", async () => {
const onWorkflowStepsChange = vi.fn();
const { rerender } = render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-102"]}
onWorkflowStepsChange={onWorkflowStepsChange}
/>,
);
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
const stepCheckbox = (await screen.findByTestId("workflow-step-checkbox-WS-101")).querySelector("input") as HTMLInputElement;
fireEvent.click(stepCheckbox);
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-102", "WS-101"]);
onWorkflowStepsChange.mockClear();
rerender(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-101"]}
onWorkflowStepsChange={onWorkflowStepsChange}
/>,
);
if (!screen.queryByTestId("workflow-steps-editor")) {
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
}
const selectedCheckbox = (await screen.findByTestId("workflow-step-checkbox-WS-101")).querySelector("input") as HTMLInputElement;
expect(selectedCheckbox.checked).toBe(true);
fireEvent.click(selectedCheckbox);
expect(onWorkflowStepsChange).toHaveBeenCalledWith([]);
});
it("reorders selected workflow steps with move buttons", async () => {
const onWorkflowStepsChange = vi.fn();
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-101", "WS-102"]}
onWorkflowStepsChange={onWorkflowStepsChange}
/>,
);
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
await screen.findByTestId("workflow-step-order");
fireEvent.click(screen.getByTestId("workflow-step-move-down-WS-101"));
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-102", "WS-101"]);
});
it("removes a selected workflow step from execution order", async () => {
const onWorkflowStepsChange = vi.fn();
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-101", "WS-102"]}
onWorkflowStepsChange={onWorkflowStepsChange}
/>,
);
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
await screen.findByTestId("workflow-step-order");
fireEvent.click(screen.getByTestId("workflow-step-remove-WS-101"));
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-102"]);
});
it("shows both results and edit UI when editing with existing results", async () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={mockResults}
canEdit
enabledWorkflowSteps={["WS-101"]}
/>,
);
expect(screen.getByTestId("workflow-results-list")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
expect(screen.getByTestId("workflow-results-list")).toBeInTheDocument();
expect(screen.getByTestId("workflow-steps-editor")).toBeInTheDocument();
await screen.findByTestId("workflow-step-checkbox-WS-101");
});
it("renders Browser Verification exactly once when fetched steps include the template-backed option", async () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-103"]}
/>,
);
fireEvent.click(screen.getByTestId("workflow-steps-edit-toggle"));
const editor = await screen.findByTestId("workflow-steps-editor");
await screen.findByTestId("workflow-step-checkbox-WS-103");
expect(screen.queryByTestId("browser-verification-checkbox")).not.toBeInTheDocument();
expect(within(editor).getAllByText("Browser Verification")).toHaveLength(1);
});
it("fetches workflow step definitions when canEdit and projectId are provided", async () => {
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
projectId="proj-123"
enabledWorkflowSteps={[]}
/>,
);
await waitFor(() => {
expect(mockedFetchWorkflowSteps).toHaveBeenCalledWith("proj-123");
});
});
});
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\(/);
});
it("keeps workflow tab CSS selector blocks free of raw color literals", () => {
const css = loadAllAppCssBaseOnly();
const selectors = [
".workflow-result-badge--passed",
".workflow-result-badge--failed",
".workflow-result-badge--pending",
".phase-badge--pre-merge",
".phase-badge--post-merge",
".workflow-result-output",
".workflow-live-log-tool",
".workflow-live-log-tool-result",
".workflow-live-log-tool-error",
".workflow-output-modal-overlay",
];
for (const selector of selectors) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = css.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
expect(match?.[1] ?? "").not.toMatch(/#[0-9a-fA-F]{3,8}|rgba?\(/);
}
});
it("wraps configured workflow names and modal headers to prevent long-name overflow", () => {
const css = loadAllAppCssBaseOnly();
expect(css).toMatch(/\.workflow-configured-title-row\s*\{[^}]*flex-wrap:\s*wrap;[^}]*min-width:\s*0;/);
expect(css).toMatch(/\.workflow-configured-name\s*\{[^}]*flex-wrap:\s*wrap;[^}]*min-width:\s*0;/);
expect(css).toMatch(/\.workflow-configured-name-text\s*\{[^}]*min-width:\s*0;[^}]*overflow-wrap:\s*anywhere;/);
expect(css).toMatch(/\.workflow-output-modal-header\s*\{[^}]*flex-wrap:\s*wrap;/);
expect(css).toMatch(/\.workflow-output-modal-title\s*\{[^}]*flex-wrap:\s*wrap;[^}]*min-width:\s*0;/);
expect(css).toMatch(/\.workflow-output-modal-name\s*\{[^}]*min-width:\s*0;[^}]*overflow-wrap:\s*anywhere;/);
});
it("keeps workflow output header actions visible without hardcoded badge sizing", () => {
const baseCss = loadAllAppCssBaseOnly();
const allCss = loadAllAppCss();
expect(baseCss).toMatch(/\.phase-badge\s*\{[^}]*font-size:\s*calc\(var\(--space-sm\) \+ var\(--space-xs\) \* 0\.75\);/);
expect(baseCss).toMatch(/\.workflow-result-output-header\s*\{[^}]*flex-wrap:\s*wrap;/);
expect(baseCss).toMatch(/\.workflow-result-output-preview\s*\{[^}]*flex:\s*1 1 auto;[^}]*min-width:\s*0;[^}]*overflow-wrap:\s*anywhere;/);
expect(allCss).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-result-output-preview\s*\{[^}]*flex-basis:\s*100%;[^}]*order:\s*3;/);
expect(baseCss).toMatch(/\.workflow-result-mode-toggle\s*\{[^}]*margin-left:\s*auto;[^}]*flex-shrink:\s*0;/);
expect(allCss).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-result-mode-toggle\s*\{[^}]*margin-left:\s*0;[^}]*min-width:\s*calc\(var\(--space-lg\) \* 2 \+ var\(--space-xs\)\);[^}]*min-height:\s*calc\(var\(--space-lg\) \* 2 \+ var\(--space-xs\)\);/);
});
it("keeps the workflow edit toggle on button primitives instead of fixed icon-button sizing", () => {
const baseCss = loadAllAppCssBaseOnly();
const editToggleRule = baseCss.match(/\.workflow-results-edit-toggle\s*\{([^}]*)\}/)?.[1] ?? "";
const buttonSmallRule = baseCss.match(/\.btn-sm\s*\{([^}]*)\}/)?.[1] ?? "";
expect(editToggleRule).not.toMatch(/\bwidth\s*:\s*28px\s*;/);
expect(editToggleRule).not.toMatch(/\bheight\s*:\s*28px\s*;/);
expect(buttonSmallRule).toMatch(/padding\s*:\s*(?!0(?:\s+0){0,3})[^;]+;/);
});
it("allows workflow modal controls to wrap on mobile so the close button stays visible", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-controls\s*\{[^}]*width:\s*100%;[^}]*justify-content:\s*space-between;/);
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-configured-header \.workflow-results-edit-toggle\s*\{[^}]*width:\s*100%;[^}]*justify-content:\s*center;/);
});
it("applies fullscreen modal dimensions on mobile", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;[^}]*border-radius:\s*0;/);
});
it("removes mobile modal overlay inset padding", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-overlay\s*\{[^}]*padding:\s*0;/);
});
it("includes safe-area top padding for expanded output modal header on mobile", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-header\s*\{[^}]*padding-top:\s*max\([^;]*env\(safe-area-inset-top/);
});
it("includes safe-area bottom padding for expanded output modal body on mobile", () => {
const css = loadAllAppCss();
expect(css).toMatch(/@media \(max-width: 768px\)\s*\{[\s\S]*?\.workflow-output-modal-body\s*\{[^}]*padding-bottom:\s*calc\([^;]*env\(safe-area-inset-bottom/);
});
});
describe("expanded view modal", () => {
it("opens expanded view when zoom button is clicked", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// First expand the output
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
// Then click the expand button
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-001"));
// Modal should be visible
expect(screen.getByTestId("workflow-output-modal")).toBeInTheDocument();
expect(screen.getByTestId("workflow-output-modal-content")).toBeInTheDocument();
});
it("shows modal header with step name and phase badge", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand and open modal
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-001"));
// Check header content - use more specific selector
expect(screen.getByTestId("workflow-output-modal")).toHaveTextContent("QA Check");
expect(screen.getByTestId("workflow-output-modal-phase-WS-001")).toHaveTextContent("Pre-merge");
});
it("has a close button that closes the modal", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand and open modal
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-001"));
// Modal is open
expect(screen.getByTestId("workflow-output-modal")).toBeInTheDocument();
// Click close button
fireEvent.click(screen.getByTestId("workflow-output-modal-close"));
// Modal should be closed
expect(screen.queryByTestId("workflow-output-modal")).not.toBeInTheDocument();
});
it("closes modal when clicking backdrop", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand and open modal
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-001"));
// Modal is open
expect(screen.getByTestId("workflow-output-modal")).toBeInTheDocument();
// Click backdrop (overlay)
const overlay = screen.getByTestId("workflow-output-modal");
fireEvent.click(overlay);
// Modal should be closed (clicking backdrop should close)
// Note: The actual click handler checks if target === currentTarget
// In the DOM, clicking the overlay div itself triggers the close
});
it("modal syncs with step render mode", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand WS-001 and toggle to plain mode
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
fireEvent.click(screen.getByTestId("workflow-result-mode-toggle-WS-001"));
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Plain");
// Open modal
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-001"));
// Modal should also be in plain mode
expect(screen.getByTestId("workflow-output-modal-mode-toggle")).toHaveTextContent("Plain");
});
it("can toggle render mode within modal", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand and open modal (starts in markdown mode)
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-001"));
// Modal is in markdown mode
expect(screen.getByTestId("workflow-output-modal-mode-toggle")).toHaveTextContent("Markdown");
// Toggle to plain in modal
fireEvent.click(screen.getByTestId("workflow-output-modal-mode-toggle"));
expect(screen.getByTestId("workflow-output-modal-mode-toggle")).toHaveTextContent("Plain");
// The inline view should also reflect this change
expect(screen.getByTestId("workflow-result-mode-toggle-WS-001")).toHaveTextContent("Plain");
});
it("displays markdown content in expanded view", () => {
const markdownResult: WorkflowStepResult[] = [
{
workflowStepId: "WS-MD",
workflowStepName: "Markdown Check",
status: "passed",
output: "# Header\n\n- Item 1\n- Item 2",
},
];
render(<WorkflowResultsTab taskId="FN-001" results={markdownResult} />);
// Expand and open modal
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-MD"));
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-MD"));
// Modal content should be rendered
expect(screen.getByTestId("workflow-output-modal-content")).toBeInTheDocument();
});
it("does not show expand button when output is collapsed", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand button should not be visible when output is collapsed
expect(screen.queryByTestId("workflow-result-expand-WS-001")).not.toBeInTheDocument();
});
it("modal is independent per step", () => {
render(<WorkflowResultsTab taskId="FN-001" results={mockResults} />);
// Expand both
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-001"));
fireEvent.click(screen.getByTestId("workflow-result-toggle-WS-002"));
// Open modal for WS-001
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-001"));
expect(screen.getByTestId("workflow-output-modal")).toBeInTheDocument();
// Close modal
fireEvent.click(screen.getByTestId("workflow-output-modal-close"));
expect(screen.queryByTestId("workflow-output-modal")).not.toBeInTheDocument();
// Open modal for WS-002
fireEvent.click(screen.getByTestId("workflow-result-expand-WS-002"));
expect(screen.getByTestId("workflow-output-modal")).toBeInTheDocument();
});
});
});