feat(FN-691): merge Steering and Comments tabs into unified Comments tab
- Remove SteeringTab component and merge steering comments into TaskComments - Consolidate both steering comments (pinned) and regular comments in one tab - Update TaskDetailModal to render single Comments tab instead of separate Steering/Comments tabs - Migrate SteeringTab tests into TaskComments test suite with full coverage - Update TaskDetailModal tests for new 6-tab layout (Definition, Activity, Agent Log, Changes, Comments, Model)
This commit is contained in:
@@ -1,169 +0,0 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import type { TaskDetail } from "@fusion/core";
|
||||
import { addComment } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
function formatTimestamp(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
|
||||
if (diffMin < 1) return "just now";
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
if (diffHr < 24) return `${diffHr}h ago`;
|
||||
if (diffDay < 7) return `${diffDay}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
interface SteeringTabProps {
|
||||
task: TaskDetail;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
}
|
||||
|
||||
export function SteeringTab({ task, addToast }: SteeringTabProps) {
|
||||
const [comments, setComments] = useState(task.comments || []);
|
||||
const [newComment, setNewComment] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const MAX_LENGTH = 2000;
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!newComment.trim() || newComment.length > MAX_LENGTH || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const updated = await addComment(task.id, newComment.trim());
|
||||
setComments(updated.comments || []);
|
||||
setNewComment("");
|
||||
addToast("Comment added", "success");
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [task.id, newComment, isSubmitting, addToast]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
);
|
||||
|
||||
const isValid = newComment.trim().length > 0 && newComment.length <= MAX_LENGTH;
|
||||
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<h4>Comments</h4>
|
||||
<p style={{ fontSize: "13px", opacity: 0.7, marginBottom: "12px" }}>
|
||||
Add comments to guide the AI during task execution. These are injected into the execution context.
|
||||
</p>
|
||||
|
||||
{comments.length > 0 ? (
|
||||
<div className="detail-activity-list" style={{ marginBottom: "16px" }}>
|
||||
{[...comments].reverse().map((comment) => (
|
||||
<div key={comment.id} className="detail-log-entry">
|
||||
<div className="detail-log-header">
|
||||
<span
|
||||
className="detail-log-timestamp"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
padding: "2px 6px",
|
||||
borderRadius: "4px",
|
||||
background:
|
||||
comment.author === "user"
|
||||
? "var(--accent-primary, #6366f1)"
|
||||
: "var(--accent-secondary, #8b5cf6)",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{comment.author}
|
||||
</span>
|
||||
{formatTimestamp(comment.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "4px",
|
||||
padding: "8px 12px",
|
||||
background: "var(--bg-secondary)",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid var(--border, #333)",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.5",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{comment.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ opacity: 0.5, marginBottom: "16px" }}>(no comments yet)</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
<textarea
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Add a comment... (Ctrl+Enter to submit)"
|
||||
maxLength={MAX_LENGTH}
|
||||
rows={4}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
fontSize: "14px",
|
||||
fontFamily: "inherit",
|
||||
background: "var(--bg-secondary)",
|
||||
border: "1px solid var(--border, #333)",
|
||||
borderRadius: "6px",
|
||||
color: "inherit",
|
||||
resize: "vertical",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
opacity: newComment.length > MAX_LENGTH ? 0.9 : 0.5,
|
||||
color:
|
||||
newComment.length > MAX_LENGTH
|
||||
? "var(--error, #ef4444)"
|
||||
: "inherit",
|
||||
}}
|
||||
>
|
||||
{newComment.length} / {MAX_LENGTH}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Adding…" : "Add Comment"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { Task, TaskComment } from "@fusion/core";
|
||||
import { addTaskComment, updateTaskComment, deleteTaskComment } from "../api";
|
||||
import { addComment, addTaskComment, updateTaskComment, deleteTaskComment } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
type CommentType = "user" | "steering";
|
||||
|
||||
interface TaskCommentsProps {
|
||||
task: Task;
|
||||
onTaskUpdated?: (task: Task) => void;
|
||||
@@ -16,21 +18,64 @@ function formatCommentTimestamp(comment: TaskComment): string {
|
||||
return comment.updatedAt ? `${label} (edited)` : label;
|
||||
}
|
||||
|
||||
function formatRelativeTimestamp(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
|
||||
if (diffMin < 1) return "just now";
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
if (diffHr < 24) return `${diffHr}h ago`;
|
||||
if (diffDay < 7) return `${diffDay}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
const MAX_LENGTH = 2000;
|
||||
|
||||
export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "user" }: TaskCommentsProps) {
|
||||
const [draft, setDraft] = useState("");
|
||||
const [commentType, setCommentType] = useState<CommentType>("user");
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editingText, setEditingText] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
// Unified comments from task.comments (includes migrated steering comments)
|
||||
const comments = useMemo(() => task.comments || [], [task.comments]);
|
||||
|
||||
async function handleAddComment() {
|
||||
// Legacy steering comments (if any still exist on the task)
|
||||
const steeringComments = useMemo(() => task.steeringComments || [], [task.steeringComments]);
|
||||
|
||||
// All comments combined, sorted newest first
|
||||
const allComments = useMemo(() => {
|
||||
const combined = [...comments, ...steeringComments];
|
||||
return combined.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
}, [comments, steeringComments]);
|
||||
|
||||
// Determine if a comment is a steering/AI guidance comment
|
||||
const isSteeringComment = useCallback((comment: TaskComment): boolean => {
|
||||
// Check if from the steeringComments array
|
||||
if (steeringComments.some(sc => sc.id === comment.id)) return true;
|
||||
// Check if the author indicates it's an agent/AI comment
|
||||
if (comment.author === "agent" || comment.author === "system") return true;
|
||||
return false;
|
||||
}, [steeringComments]);
|
||||
|
||||
const handleAddComment = useCallback(async () => {
|
||||
const text = draft.trim();
|
||||
if (!text) return;
|
||||
if (!text || text.length > MAX_LENGTH || submitting) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await addTaskComment(task.id, text, currentAuthor);
|
||||
let updated: Task;
|
||||
if (commentType === "steering") {
|
||||
updated = await addComment(task.id, text);
|
||||
} else {
|
||||
updated = await addTaskComment(task.id, text, currentAuthor);
|
||||
}
|
||||
setDraft("");
|
||||
onTaskUpdated?.(updated);
|
||||
addToast("Comment added", "success");
|
||||
@@ -39,7 +84,17 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
}, [draft, commentType, submitting, task.id, currentAuthor, onTaskUpdated, addToast]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
void handleAddComment();
|
||||
}
|
||||
},
|
||||
[handleAddComment]
|
||||
);
|
||||
|
||||
async function handleSaveEdit(commentId: string) {
|
||||
const text = editingText.trim();
|
||||
@@ -71,23 +126,45 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = draft.trim().length > 0 && draft.length <= MAX_LENGTH;
|
||||
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<h4>Comments</h4>
|
||||
{comments.length === 0 ? (
|
||||
|
||||
{allComments.length === 0 ? (
|
||||
<div className="detail-log-empty">No comments yet.</div>
|
||||
) : (
|
||||
<div className="detail-activity-list">
|
||||
{comments.map((comment) => {
|
||||
const canEdit = comment.author === currentAuthor;
|
||||
{allComments.map((comment) => {
|
||||
const isSteering = isSteeringComment(comment);
|
||||
const canEdit = !isSteering && comment.author === currentAuthor;
|
||||
const isEditing = editingId === comment.id;
|
||||
return (
|
||||
<div key={comment.id} className="detail-log-entry">
|
||||
<div className="detail-log-header" style={{ justifyContent: "space-between", gap: 12 }}>
|
||||
<div>
|
||||
<strong>{comment.author}</strong>
|
||||
<span className="detail-log-timestamp" style={{ marginLeft: 8 }}>
|
||||
{formatCommentTimestamp(comment)}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
{isSteering ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
padding: "2px 6px",
|
||||
borderRadius: "4px",
|
||||
background: "var(--accent-secondary, #8b5cf6)",
|
||||
color: "#fff",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
data-testid="ai-guidance-badge"
|
||||
>
|
||||
AI Guidance
|
||||
</span>
|
||||
) : (
|
||||
<strong>{comment.author}</strong>
|
||||
)}
|
||||
<span className="detail-log-timestamp">
|
||||
{isSteering
|
||||
? formatRelativeTimestamp(comment.createdAt)
|
||||
: formatCommentTimestamp(comment)}
|
||||
</span>
|
||||
</div>
|
||||
{canEdit && !isEditing ? (
|
||||
@@ -137,7 +214,16 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="detail-log-outcome" style={{ whiteSpace: "pre-wrap" }}>
|
||||
<div
|
||||
className="detail-log-outcome"
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
...(isSteering ? {
|
||||
borderLeft: "3px solid var(--accent-secondary, #8b5cf6)",
|
||||
paddingLeft: "12px",
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
{comment.text}
|
||||
</div>
|
||||
)}
|
||||
@@ -148,16 +234,61 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
)}
|
||||
|
||||
<div style={{ display: "grid", gap: 8, marginTop: 12 }}>
|
||||
{/* Comment type selector */}
|
||||
<div style={{ display: "flex", gap: 4 }}>
|
||||
<button
|
||||
className={`btn btn-sm${commentType === "user" ? " btn-primary" : ""}`}
|
||||
onClick={() => setCommentType("user")}
|
||||
type="button"
|
||||
>
|
||||
Comment
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-sm${commentType === "steering" ? " btn-primary" : ""}`}
|
||||
onClick={() => setCommentType("steering")}
|
||||
type="button"
|
||||
>
|
||||
AI Guidance
|
||||
</button>
|
||||
</div>
|
||||
{commentType === "steering" && (
|
||||
<p style={{ fontSize: "12px", opacity: 0.7, margin: 0 }}>
|
||||
AI Guidance comments are injected into the task execution context to guide the agent.
|
||||
</p>
|
||||
)}
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={3}
|
||||
placeholder="Add a comment"
|
||||
placeholder={commentType === "steering"
|
||||
? "Add guidance for the AI agent… (Ctrl+Enter to submit)"
|
||||
: "Add a comment… (Ctrl+Enter to submit)"}
|
||||
className="spec-editor-feedback"
|
||||
maxLength={MAX_LENGTH}
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => void handleAddComment()} disabled={submitting || !draft.trim()}>
|
||||
{submitting ? "Posting…" : "Add Comment"}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
opacity: draft.length > MAX_LENGTH ? 0.9 : 0.5,
|
||||
color: draft.length > MAX_LENGTH ? "var(--error, #ef4444)" : "inherit",
|
||||
}}
|
||||
>
|
||||
{draft.length} / {MAX_LENGTH}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => void handleAddComment()}
|
||||
disabled={!isValid || submitting}
|
||||
>
|
||||
{submitting ? "Posting…" : commentType === "steering" ? "Add Guidance" : "Add Comment"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { uploadAttachment, deleteAttachment, updateTask, pauseTask, unpauseTask,
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useAgentLogs } from "../hooks/useAgentLogs";
|
||||
import { AgentLogViewer } from "./AgentLogViewer";
|
||||
import { SteeringTab } from "./SteeringTab";
|
||||
import { ModelSelectorTab } from "./ModelSelectorTab";
|
||||
import { PrSection } from "./PrSection";
|
||||
import { TaskComments } from "./TaskComments";
|
||||
@@ -106,7 +105,7 @@ export function TaskDetailModal({
|
||||
addToast,
|
||||
githubTokenConfigured,
|
||||
}: TaskDetailModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"definition" | "activity" | "agent-log" | "changes" | "steering" | "comments" | "model">("definition");
|
||||
const [activeTab, setActiveTab] = useState<"definition" | "activity" | "agent-log" | "changes" | "comments" | "model">("definition");
|
||||
const [attachments, setAttachments] = useState<TaskAttachment[]>(task.attachments || []);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [dependencies, setDependencies] = useState<string[]>(task.dependencies || []);
|
||||
@@ -680,12 +679,6 @@ export function TaskDetailModal({
|
||||
Changes
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`detail-tab${activeTab === "steering" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("steering")}
|
||||
>
|
||||
Steering
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "comments" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("comments")}
|
||||
@@ -714,8 +707,6 @@ export function TaskDetailModal({
|
||||
</div>
|
||||
) : activeTab === "changes" ? (
|
||||
<TaskChangesTab taskId={task.id} worktree={task.worktree} />
|
||||
) : activeTab === "steering" ? (
|
||||
<SteeringTab task={task} addToast={addToast} />
|
||||
) : activeTab === "comments" ? (
|
||||
<TaskComments task={task} addToast={addToast} />
|
||||
) : activeTab === "activity" ? (
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { SteeringTab } from "../SteeringTab";
|
||||
import type { TaskDetail } from "@fusion/core";
|
||||
|
||||
// Mock the API module
|
||||
vi.mock("../../api", () => ({
|
||||
addComment: vi.fn(),
|
||||
}));
|
||||
|
||||
import { addComment } from "../../api";
|
||||
|
||||
const mockAddToast = vi.fn();
|
||||
|
||||
function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||
return {
|
||||
id: "FN-001",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
prompt: "# Test\n\nTest prompt",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SteeringTab", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders empty state when no comments", () => {
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
expect(screen.getByText("Comments")).toBeTruthy();
|
||||
expect(screen.getByText(/no comments yet/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders comments in reverse chronological order", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{
|
||||
id: "1",
|
||||
text: "First comment",
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
author: "user",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
text: "Second comment",
|
||||
createdAt: "2024-01-02T00:00:00Z",
|
||||
author: "agent",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SteeringTab task={task} addToast={mockAddToast} />);
|
||||
|
||||
// Comments should be in reverse order (newest first)
|
||||
const comments = screen.getAllByText(/comment$/);
|
||||
expect(comments.length).toBe(2);
|
||||
expect(comments[0].textContent).toBe("Second comment");
|
||||
expect(comments[1].textContent).toBe("First comment");
|
||||
});
|
||||
|
||||
it("shows author badges for comments", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{ id: "1", text: "User comment", createdAt: "2024-01-01T00:00:00Z", author: "user" },
|
||||
{ id: "2", text: "Agent comment", createdAt: "2024-01-02T00:00:00Z", author: "agent" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<SteeringTab task={task} addToast={mockAddToast} />);
|
||||
|
||||
expect(screen.getByText("user")).toBeTruthy();
|
||||
expect(screen.getByText("agent")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows character count", () => {
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Hello" } });
|
||||
|
||||
expect(screen.getByText("5 / 2000")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables submit button when textarea is empty", () => {
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
expect(button.hasAttribute("disabled")).toBe(true);
|
||||
});
|
||||
|
||||
it("disables submit button when text exceeds 2000 characters", () => {
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
const longText = "a".repeat(2001);
|
||||
fireEvent.change(textarea, { target: { value: longText } });
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
expect(button.hasAttribute("disabled")).toBe(true);
|
||||
});
|
||||
|
||||
it("enables submit button when text is valid", () => {
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Valid comment" } });
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
expect(button.hasAttribute("disabled")).toBe(false);
|
||||
});
|
||||
|
||||
it("submits comment on button click", async () => {
|
||||
const mockApi = vi.mocked(addComment);
|
||||
mockApi.mockResolvedValue({
|
||||
...makeTask(),
|
||||
comments: [
|
||||
{
|
||||
id: "new-1",
|
||||
text: "New comment",
|
||||
createdAt: "2024-01-03T00:00:00Z",
|
||||
author: "user",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "New comment" } });
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApi).toHaveBeenCalledWith("FN-001", "New comment");
|
||||
});
|
||||
});
|
||||
|
||||
it("submits comment on Ctrl+Enter", async () => {
|
||||
const mockApi = vi.mocked(addComment);
|
||||
mockApi.mockResolvedValue({
|
||||
...makeTask(),
|
||||
comments: [
|
||||
{
|
||||
id: "new-1",
|
||||
text: "Keyboard comment",
|
||||
createdAt: "2024-01-03T00:00:00Z",
|
||||
author: "user",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Keyboard comment" } });
|
||||
|
||||
// Ctrl+Enter should submit
|
||||
fireEvent.keyDown(textarea, { key: "Enter", ctrlKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApi).toHaveBeenCalledWith("FN-001", "Keyboard comment");
|
||||
});
|
||||
});
|
||||
|
||||
it("submits comment on Cmd+Enter (Mac)", async () => {
|
||||
const mockApi = vi.mocked(addComment);
|
||||
mockApi.mockResolvedValue({
|
||||
...makeTask(),
|
||||
comments: [
|
||||
{
|
||||
id: "new-1",
|
||||
text: "Mac keyboard comment",
|
||||
createdAt: "2024-01-03T00:00:00Z",
|
||||
author: "user",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Mac keyboard comment" } });
|
||||
|
||||
// Cmd+Enter should submit (metaKey is Cmd on Mac)
|
||||
fireEvent.keyDown(textarea, { key: "Enter", metaKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApi).toHaveBeenCalledWith("FN-001", "Mac keyboard comment");
|
||||
});
|
||||
});
|
||||
|
||||
it("clears textarea after successful submission", async () => {
|
||||
const mockApi = vi.mocked(addComment);
|
||||
mockApi.mockResolvedValue({
|
||||
...makeTask(),
|
||||
comments: [
|
||||
{
|
||||
id: "new-1",
|
||||
text: "Cleared comment",
|
||||
createdAt: "2024-01-03T00:00:00Z",
|
||||
author: "user",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/) as HTMLTextAreaElement;
|
||||
fireEvent.change(textarea, { target: { value: "Cleared comment" } });
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(textarea.value).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows loading state during submission", async () => {
|
||||
const mockApi = vi.mocked(addComment);
|
||||
// Delay the resolution to see loading state
|
||||
mockApi.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
|
||||
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Loading test" } });
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
fireEvent.click(button);
|
||||
|
||||
// Should show loading text
|
||||
expect(screen.getByRole("button", { name: /Adding…/ })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows error toast on API failure", async () => {
|
||||
const mockApi = vi.mocked(addComment);
|
||||
mockApi.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Error test" } });
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddToast).toHaveBeenCalledWith("Network error", "error");
|
||||
});
|
||||
});
|
||||
|
||||
it("updates comment list after successful submission", async () => {
|
||||
const mockApi = vi.mocked(addComment);
|
||||
mockApi.mockResolvedValue({
|
||||
...makeTask(),
|
||||
comments: [
|
||||
{
|
||||
id: "new-1",
|
||||
text: "Added comment",
|
||||
createdAt: "2024-01-03T00:00:00Z",
|
||||
author: "user",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SteeringTab task={makeTask()} addToast={mockAddToast} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Added comment" } });
|
||||
|
||||
const button = screen.getByRole("button", { name: /Add Comment/ });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Added comment")).toBeTruthy();
|
||||
expect(screen.getByText("user")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { TaskComments } from "../TaskComments";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
addComment: vi.fn(),
|
||||
addTaskComment: vi.fn(),
|
||||
updateTaskComment: vi.fn(),
|
||||
deleteTaskComment: vi.fn(),
|
||||
}));
|
||||
|
||||
import { addTaskComment, updateTaskComment, deleteTaskComment } from "../../api";
|
||||
import { addComment, addTaskComment, updateTaskComment, deleteTaskComment } from "../../api";
|
||||
|
||||
const makeTask = (overrides: any = {}) => ({
|
||||
id: "FN-001",
|
||||
@@ -24,17 +25,21 @@ const makeTask = (overrides: any = {}) => ({
|
||||
});
|
||||
|
||||
describe("TaskComments", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders empty state", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
expect(screen.getByText("No comments yet.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("adds a comment", async () => {
|
||||
it("adds a user comment via addTaskComment API", async () => {
|
||||
const onTaskUpdated = vi.fn();
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({ comments: [{ id: "c1", text: "Hello", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }] }));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} onTaskUpdated={onTaskUpdated} />);
|
||||
fireEvent.change(screen.getByPlaceholderText("Add a comment"), { target: { value: "Hello" } });
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "Hello" } });
|
||||
fireEvent.click(screen.getByText("Add Comment"));
|
||||
|
||||
await waitFor(() => expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Hello", "user"));
|
||||
@@ -64,4 +69,238 @@ describe("TaskComments", () => {
|
||||
await waitFor(() => expect(deleteTaskComment).toHaveBeenCalledWith("FN-001", "c1"));
|
||||
expect(onTaskUpdated).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// --- New tests for merged steering + user comments ---
|
||||
|
||||
describe("AI Guidance comments", () => {
|
||||
it("renders AI Guidance badge for agent-authored comments", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{ id: "c1", text: "User note", author: "user", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
{ id: "c2", text: "Agent guidance", author: "agent", createdAt: "2026-01-02T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<TaskComments task={task} addToast={vi.fn()} />);
|
||||
|
||||
const badges = screen.getAllByTestId("ai-guidance-badge");
|
||||
expect(badges.length).toBe(1);
|
||||
expect(badges[0].textContent).toBe("AI Guidance");
|
||||
// User comment should show author name, not badge
|
||||
expect(screen.getByText("user")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders AI Guidance badge for system-authored comments", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{ id: "c1", text: "System message", author: "system", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<TaskComments task={task} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId("ai-guidance-badge")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show edit/delete buttons for AI Guidance comments", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{ id: "c1", text: "Agent guidance", author: "agent", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<TaskComments task={task} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.queryByText("Edit")).toBeNull();
|
||||
expect(screen.queryByText("Delete")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows edit/delete buttons only for user-authored comments", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{ id: "c1", text: "User note", author: "user", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
{ id: "c2", text: "Agent guidance", author: "agent", createdAt: "2026-01-02T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<TaskComments task={task} addToast={vi.fn()} />);
|
||||
|
||||
// Only one set of edit/delete buttons (for user comment)
|
||||
expect(screen.getAllByText("Edit").length).toBe(1);
|
||||
expect(screen.getAllByText("Delete").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("comment type selector", () => {
|
||||
it("shows Comment and AI Guidance type selector buttons", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText("Comment")).toBeTruthy();
|
||||
expect(screen.getByText("AI Guidance")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("defaults to Comment type", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
const commentBtn = screen.getByText("Comment");
|
||||
expect(commentBtn.className).toContain("btn-primary");
|
||||
});
|
||||
|
||||
it("shows helper text when AI Guidance type is selected", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Click AI Guidance button
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
|
||||
expect(screen.getByText(/AI Guidance comments are injected into the task execution context/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses addComment API when AI Guidance type is selected", async () => {
|
||||
vi.mocked(addComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "Guidance text", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Select AI Guidance type
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
|
||||
// Enter text and submit
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add guidance/), { target: { value: "Guidance text" } });
|
||||
fireEvent.click(screen.getByText("Add Guidance"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addComment).toHaveBeenCalledWith("FN-001", "Guidance text");
|
||||
expect(addTaskComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("uses addTaskComment API when Comment type is selected", async () => {
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "User text", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Comment type is default
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "User text" } });
|
||||
fireEvent.click(screen.getByText("Add Comment"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "User text", "user");
|
||||
expect(addComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("changes placeholder text based on selected type", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Default: Comment type
|
||||
expect(screen.getByPlaceholderText(/Add a comment/)).toBeTruthy();
|
||||
|
||||
// Switch to AI Guidance
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
expect(screen.getByPlaceholderText(/Add guidance for the AI agent/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("changes submit button label based on selected type", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
// Default: "Add Comment" button
|
||||
expect(screen.getByText("Add Comment")).toBeTruthy();
|
||||
|
||||
// Enable submit
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "text" } });
|
||||
|
||||
// Switch to AI Guidance
|
||||
fireEvent.click(screen.getByText("AI Guidance"));
|
||||
expect(screen.getByText("Add Guidance")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("character count", () => {
|
||||
it("shows character count", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Hello" } });
|
||||
|
||||
expect(screen.getByText("5 / 2000")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("disables submit button when text exceeds max length", () => {
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "a".repeat(2001) } });
|
||||
|
||||
const button = screen.getByText("Add Comment");
|
||||
expect(button.hasAttribute("disabled")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyboard shortcuts", () => {
|
||||
it("submits comment on Ctrl+Enter", async () => {
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "Keyboard", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Keyboard" } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter", ctrlKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Keyboard", "user");
|
||||
});
|
||||
});
|
||||
|
||||
it("submits comment on Cmd+Enter", async () => {
|
||||
vi.mocked(addTaskComment).mockResolvedValue(makeTask({
|
||||
comments: [{ id: "c1", text: "Mac", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }],
|
||||
}));
|
||||
|
||||
render(<TaskComments task={makeTask()} addToast={vi.fn()} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Add a comment/);
|
||||
fireEvent.change(textarea, { target: { value: "Mac" } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter", metaKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Mac", "user");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("comments display order", () => {
|
||||
it("sorts comments newest first", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{ id: "c1", text: "First comment", author: "user", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
{ id: "c2", text: "Second comment", author: "user", createdAt: "2026-01-02T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<TaskComments task={task} addToast={vi.fn()} />);
|
||||
|
||||
const commentTexts = screen.getAllByText(/comment$/);
|
||||
expect(commentTexts[0].textContent).toBe("Second comment");
|
||||
expect(commentTexts[1].textContent).toBe("First comment");
|
||||
});
|
||||
|
||||
it("displays both user and AI guidance comments together", () => {
|
||||
const task = makeTask({
|
||||
comments: [
|
||||
{ id: "c1", text: "User comment", author: "user", createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
{ id: "c2", text: "Agent guidance", author: "agent", createdAt: "2026-01-02T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<TaskComments task={task} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText("User comment")).toBeTruthy();
|
||||
expect(screen.getByText("Agent guidance")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -761,18 +761,16 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
|
||||
expect(container.querySelector(".detail-activity")).toBeNull();
|
||||
|
||||
// Switch to Steering tab
|
||||
fireEvent.click(screen.getByText("Steering"));
|
||||
// The SteeringTab renders an h4 with "Comments"
|
||||
expect(container.querySelector("h4")?.textContent).toBe("Comments");
|
||||
// Switch to Comments tab
|
||||
fireEvent.click(screen.getByText("Comments"));
|
||||
expect(screen.getByPlaceholderText(/Add a comment/)).toBeTruthy();
|
||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
|
||||
|
||||
// Switch back to Definition tab
|
||||
fireEvent.click(screen.getByText("Definition"));
|
||||
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
||||
expect(container.querySelector(".detail-activity")).toBeNull();
|
||||
// The Comments heading should not be visible in Definition tab
|
||||
expect(container.querySelector("h4")?.textContent).not.toBe("Comments");
|
||||
|
||||
});
|
||||
|
||||
it("switches to Agent Log tab and back", async () => {
|
||||
@@ -835,7 +833,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(afterSwitch[1]).toBe(true);
|
||||
});
|
||||
|
||||
it("switches to Steering tab", async () => {
|
||||
it("switches to Comments tab", async () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||
@@ -848,10 +846,10 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Click Steering tab
|
||||
fireEvent.click(screen.getByText("Steering"));
|
||||
// Click Comments tab
|
||||
fireEvent.click(screen.getByText("Comments"));
|
||||
|
||||
// Steering content should appear - look for the h4 heading "Comments" within the SteeringTab
|
||||
// Comments content should appear
|
||||
const headings = screen.getAllByText("Comments");
|
||||
expect(headings.length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByPlaceholderText(/Add a comment/)).toBeTruthy();
|
||||
@@ -859,7 +857,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector(".markdown-body")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows Steering tab as third tab", async () => {
|
||||
it("shows Comments tab in tab list", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask()}
|
||||
@@ -873,11 +871,11 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const tabs = screen.getAllByRole("button").filter((b) =>
|
||||
["Definition", "Activity", "Agent Log", "Steering"].includes(b.textContent || "")
|
||||
["Definition", "Activity", "Agent Log", "Comments"].includes(b.textContent || "")
|
||||
);
|
||||
expect(tabs.length).toBe(4);
|
||||
expect(tabs[1].textContent).toBe("Activity");
|
||||
expect(tabs[3].textContent).toBe("Steering");
|
||||
expect(tabs[3].textContent).toBe("Comments");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1072,7 +1070,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector(".detail-step-progress")).toBeNull();
|
||||
});
|
||||
|
||||
it("step progress is hidden in Steering tab", () => {
|
||||
it("step progress is hidden in Comments tab", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
@@ -1090,10 +1088,10 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Switch to Steering tab
|
||||
fireEvent.click(screen.getByText("Steering"));
|
||||
// Switch to Comments tab
|
||||
fireEvent.click(screen.getByText("Comments"));
|
||||
|
||||
// Should not be visible in Steering tab
|
||||
// Should not be visible in Comments tab
|
||||
expect(container.querySelector(".detail-step-progress")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1168,7 +1166,7 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(7); // Definition, Activity, Agent Log, Changes, Steering, Comments, Model
|
||||
expect(tabs.length).toBe(6); // Definition, Activity, Agent Log, Changes, Comments, Model
|
||||
// Tabs should use class-based styling, not inline styles
|
||||
expect(tabs[0].classList.contains("detail-tab")).toBe(true);
|
||||
expect(tabs[0].classList.contains("detail-tab-active")).toBe(true); // Definition is default active
|
||||
@@ -1177,7 +1175,6 @@ describe("TaskDetailModal", () => {
|
||||
expect(tabs[3].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[4].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[5].classList.contains("detail-tab-active")).toBe(false);
|
||||
expect(tabs[6].classList.contains("detail-tab-active")).toBe(false);
|
||||
// Verify no inline padding/fontSize (responsive CSS controls this)
|
||||
expect((tabs[0] as HTMLElement).style.padding).toBe("");
|
||||
expect((tabs[0] as HTMLElement).style.fontSize).toBe("");
|
||||
@@ -1655,7 +1652,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows all 7 tabs in correct order with comments", () => {
|
||||
it("shows all tabs in correct order", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask()}
|
||||
@@ -1669,14 +1666,13 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(7);
|
||||
expect(tabs.length).toBe(6); // Definition, Activity, Agent Log, Changes, Comments, Model (in-progress shows Changes)
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Activity");
|
||||
expect(tabs[2].textContent).toBe("Agent Log");
|
||||
expect(tabs[3].textContent).toBe("Changes");
|
||||
expect(tabs[4].textContent).toBe("Steering");
|
||||
expect(tabs[5].textContent).toBe("Comments");
|
||||
expect(tabs[6].textContent).toBe("Model");
|
||||
expect(tabs[4].textContent).toBe("Comments");
|
||||
expect(tabs[5].textContent).toBe("Model");
|
||||
});
|
||||
|
||||
it("shows empty state and Edit button when no prompt", () => {
|
||||
|
||||
Reference in New Issue
Block a user