- Stop injecting a hardcoded Browser Verification option in WorkflowResultsTab and rely only on fetched workflow step definitions - Normalize workflow step checkbox test IDs to be id-based for all options, including template-backed Browser Verification steps - Extend WorkflowResultsTab coverage to assert Browser Verification renders exactly once when provided by API data - Increase dashboard Vitest timeout defaults and update the slow route test timeout to reduce flaky failures under concurrent workspace runs
944 lines
36 KiB
TypeScript
944 lines
36 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 type { WorkflowStep, WorkflowStepResult } from "@fusion/core";
|
|
|
|
vi.mock("../../api", () => ({
|
|
fetchWorkflowSteps: vi.fn(),
|
|
}));
|
|
|
|
const mockedFetchWorkflowSteps = vi.mocked(fetchWorkflowSteps);
|
|
|
|
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);
|
|
});
|
|
|
|
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("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("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\(/);
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|