1130 lines
44 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|