FN-7833: render task Artifacts documents as Markdown, expanded by default

Renders every task document in the Artifacts tab expanded and as rendered Markdown by default, with the Markdown/Plain preference persisted across sessions instead of resetting per document.

- Replace single expandedDocKey/expandedContent state with a multi-key expandedDocKeys Set plus a per-document revisionContentByKey map so multiple documents can be expanded simultaneously.
- Default renderMarkdown to true and persist the operator's Markdown/Plain toggle choice to localStorage (fusion.taskDocuments.renderMarkdown) via readBooleanPref/writeBooleanPref helpers.
- Auto-expand newly loaded documents while preserving collapse state for documents the user has explicitly collapsed, tracked per taskId.
- Update handleStartEdit and handleViewRevision to operate per-document-key instead of a single global expanded document.
- Add changeset (@runfusion/fusion patch) describing the Artifacts-tab default-expanded Markdown behavior.
- Rework TaskDocumentsTab tests for multi-document expand/collapse, per-card markdown rendering, and localStorage-backed preference persistence.

Files changed:
 .changeset/fn-7833-artifacts-tab-markdown-expand-default.md      |   7 +
 packages/dashboard/app/components/TaskDocumentsTab.tsx           | 149 +++++++++-----
 packages/dashboard/app/components/__tests__/TaskDocumentsTab.test.tsx | 223 +++++----------------
 3 files changed, 160 insertions(+), 219 deletions(-)

Fusion-Task-Id: FN-7833

Fusion-Task-Lineage: 1915c594-7191-47a2-962b-18136d572cf1

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-11 20:50:59 -07:00
parent 06bf0b85b9
commit 4fb360631c
3 changed files with 160 additions and 219 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Show task Artifacts-tab documents expanded with Markdown by default.
category: feature
dev: TaskDocumentsTab now uses multi-expand document state and persists the Markdown/Plain preference.

View File

@@ -22,6 +22,27 @@ import { ArtifactMedia, getArtifactTypeLabel } from "./ArtifactMedia";
// Document key validation: alphanumeric, hyphens, underscores, 1-64 chars // Document key validation: alphanumeric, hyphens, underscores, 1-64 chars
const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/; const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
const MAX_CONTENT_PREVIEW = 200; const MAX_CONTENT_PREVIEW = 200;
const TASK_DOCUMENTS_MARKDOWN_TOGGLE_STORAGE_KEY = "fusion.taskDocuments.renderMarkdown";
function readBooleanPref(key: string, defaultValue: boolean): boolean {
if (typeof window === "undefined") return defaultValue;
try {
const raw = window.localStorage.getItem(key);
if (raw === null) return defaultValue;
return raw === "true";
} catch {
return defaultValue;
}
}
function writeBooleanPref(key: string, value: boolean): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(key, value ? "true" : "false");
} catch {
// ignore storage failures (quota, private mode, etc.)
}
}
interface TaskDocumentsTabProps { interface TaskDocumentsTabProps {
taskId: string; taskId: string;
@@ -109,8 +130,8 @@ export function TaskDocumentsTab({
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const [documents, setDocuments] = useState<TaskDocument[]>([]); const [documents, setDocuments] = useState<TaskDocument[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [expandedDocKey, setExpandedDocKey] = useState<string | null>(null); const [expandedDocKeys, setExpandedDocKeys] = useState<Set<string>>(() => new Set());
const [expandedContent, setExpandedContent] = useState(""); const [revisionContentByKey, setRevisionContentByKey] = useState<Record<string, string>>({});
const [editingDocKey, setEditingDocKey] = useState<string | null>(null); const [editingDocKey, setEditingDocKey] = useState<string | null>(null);
const [editContent, setEditContent] = useState(""); const [editContent, setEditContent] = useState("");
const [showHistory, setShowHistory] = useState<string | null>(null); const [showHistory, setShowHistory] = useState<string | null>(null);
@@ -122,7 +143,11 @@ export function TaskDocumentsTab({
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [deletingKey, setDeletingKey] = useState<string | null>(null); const [deletingKey, setDeletingKey] = useState<string | null>(null);
const [confirmDelete, setConfirmDelete] = useState<string | null>(null); const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
const [renderMarkdown, setRenderMarkdown] = useState(false); /*
* FNXC:ArtifactRegistry 2026-07-11-00:00:
* FN-7833 makes task Artifacts-tab documents readable without extra clicks: render Markdown by default and persist the operator's Markdown/Plain preference for future task document views.
*/
const [renderMarkdown, setRenderMarkdown] = useState<boolean>(() => readBooleanPref(TASK_DOCUMENTS_MARKDOWN_TOGGLE_STORAGE_KEY, true));
/* /*
* FNXC:ArtifactRegistry 2026-06-29-00:00: * FNXC:ArtifactRegistry 2026-06-29-00:00:
* Task detail image artifacts must be viewable in-place from the task modal. Keep the expand target image-only so document, audio, video, and generic cards retain their current non-lightbox behavior without empty controls. * Task detail image artifacts must be viewable in-place from the task modal. Keep the expand target image-only so document, audio, video, and generic cards retain their current non-lightbox behavior without empty controls.
@@ -131,12 +156,28 @@ export function TaskDocumentsTab({
const lightboxDialogRef = useRef<HTMLDivElement>(null); const lightboxDialogRef = useRef<HTMLDivElement>(null);
const lightboxCloseRef = useRef<HTMLButtonElement>(null); const lightboxCloseRef = useRef<HTMLButtonElement>(null);
const lightboxReturnFocusRef = useRef<HTMLElement | null>(null); const lightboxReturnFocusRef = useRef<HTMLElement | null>(null);
const loadedTaskIdRef = useRef(taskId);
const documentKeysRef = useRef<Set<string>>(new Set());
const { artifacts, loading: artifactsLoading, error: artifactsError } = useArtifacts({ projectId, taskId }); const { artifacts, loading: artifactsLoading, error: artifactsError } = useArtifacts({ projectId, taskId });
const loadDocuments = useCallback(async () => { const loadDocuments = useCallback(async () => {
try { try {
const docs = await fetchTaskDocuments(taskId, projectId); const docs = await fetchTaskDocuments(taskId, projectId);
const previousKeys = loadedTaskIdRef.current === taskId ? documentKeysRef.current : new Set<string>();
const nextKeys = new Set(docs.map((doc) => doc.key));
loadedTaskIdRef.current = taskId;
documentKeysRef.current = nextKeys;
setDocuments(docs); setDocuments(docs);
setExpandedDocKeys((current) => {
const next = new Set<string>();
for (const doc of docs) {
if (current.has(doc.key) || !previousKeys.has(doc.key)) {
next.add(doc.key);
}
}
return next;
});
setRevisionContentByKey((current) => Object.fromEntries(Object.entries(current).filter(([key]) => nextKeys.has(key))));
} catch (error) { } catch (error) {
addToast(getErrorMessage(error) || t("taskDocuments.failedToLoad", "Failed to load documents"), "error"); addToast(getErrorMessage(error) || t("taskDocuments.failedToLoad", "Failed to load documents"), "error");
} finally { } finally {
@@ -154,23 +195,30 @@ export function TaskDocumentsTab({
} }
}, [addToast, artifactsError, t]); }, [addToast, artifactsError, t]);
async function handleExpandDocument(doc: TaskDocument) { useEffect(() => {
if (expandedDocKey === doc.key) { writeBooleanPref(TASK_DOCUMENTS_MARKDOWN_TOGGLE_STORAGE_KEY, renderMarkdown);
setExpandedDocKey(null); }, [renderMarkdown]);
setExpandedContent("");
setEditingDocKey(null); function handleExpandDocument(doc: TaskDocument) {
setEditContent(""); const isExpanded = expandedDocKeys.has(doc.key);
setShowHistory(null); setExpandedDocKeys((current) => {
setRevisions([]); const next = new Set(current);
setRenderMarkdown(false); if (isExpanded) {
} else { next.delete(doc.key);
setExpandedDocKey(doc.key); } else {
setExpandedContent(doc.content); next.add(doc.key);
setEditingDocKey(null); }
setEditContent(""); return next;
setShowHistory(null); });
setRevisions([]); if (isExpanded) {
setRenderMarkdown(false); if (editingDocKey === doc.key) {
setEditingDocKey(null);
setEditContent("");
}
if (showHistory === doc.key) {
setShowHistory(null);
setRevisions([]);
}
} }
} }
@@ -192,11 +240,9 @@ export function TaskDocumentsTab({
} }
} }
function handleStartEdit() { function handleStartEdit(doc: TaskDocument) {
if (expandedDocKey) { setEditingDocKey(doc.key);
setEditingDocKey(expandedDocKey); setEditContent(revisionContentByKey[doc.key] ?? doc.content);
setEditContent(expandedContent);
}
} }
function handleCancelEdit() { function handleCancelEdit() {
@@ -211,12 +257,12 @@ export function TaskDocumentsTab({
await putTaskDocument(taskId, editingDocKey, editContent, {}, projectId); await putTaskDocument(taskId, editingDocKey, editContent, {}, projectId);
setEditingDocKey(null); setEditingDocKey(null);
setEditContent(""); setEditContent("");
setRevisionContentByKey((current) => {
const next = { ...current };
delete next[editingDocKey];
return next;
});
await loadDocuments(); await loadDocuments();
// Refresh expanded content
const updated = documents.find((d) => d.key === editingDocKey);
if (updated) {
setExpandedContent(updated.content);
}
addToast(t("taskDocuments.saved", "Document saved"), "success"); addToast(t("taskDocuments.saved", "Document saved"), "success");
} catch (error) { } catch (error) {
addToast(getErrorMessage(error) || t("taskDocuments.failedToSave", "Failed to save document"), "error"); addToast(getErrorMessage(error) || t("taskDocuments.failedToSave", "Failed to save document"), "error");
@@ -264,10 +310,16 @@ export function TaskDocumentsTab({
await deleteTaskDocument(taskId, key, projectId); await deleteTaskDocument(taskId, key, projectId);
setConfirmDelete(null); setConfirmDelete(null);
setDeletingKey(null); setDeletingKey(null);
if (expandedDocKey === key) { setExpandedDocKeys((current) => {
setExpandedDocKey(null); const next = new Set(current);
setExpandedContent(""); next.delete(key);
} return next;
});
setRevisionContentByKey((current) => {
const next = { ...current };
delete next[key];
return next;
});
if (showHistory === key) { if (showHistory === key) {
setShowHistory(null); setShowHistory(null);
setRevisions([]); setRevisions([]);
@@ -281,8 +333,8 @@ export function TaskDocumentsTab({
} }
} }
function handleViewRevision(revision: TaskDocumentRevision) { function handleViewRevision(docKey: string, revision: TaskDocumentRevision) {
setExpandedContent(revision.content); setRevisionContentByKey((current) => ({ ...current, [docKey]: revision.content }));
setEditingDocKey(null); setEditingDocKey(null);
setEditContent(""); setEditContent("");
} }
@@ -468,7 +520,11 @@ export function TaskDocumentsTab({
) )
) : ( ) : (
<div className="task-documents-list"> <div className="task-documents-list">
{documents.map((doc) => ( {documents.map((doc) => {
const isExpanded = expandedDocKeys.has(doc.key);
const displayedContent = revisionContentByKey[doc.key] ?? doc.content;
return (
<div key={doc.key} className="task-document-card"> <div key={doc.key} className="task-document-card">
<div className="task-document-card-header"> <div className="task-document-card-header">
<div className="task-document-card-title"> <div className="task-document-card-title">
@@ -483,7 +539,7 @@ export function TaskDocumentsTab({
</div> </div>
{/* Expanded Content View */} {/* Expanded Content View */}
{expandedDocKey === doc.key && editingDocKey !== doc.key && ( {isExpanded && editingDocKey !== doc.key && (
<> <>
<div className="task-document-content-header"> <div className="task-document-content-header">
<button <button
@@ -500,11 +556,11 @@ export function TaskDocumentsTab({
{renderMarkdown ? ( {renderMarkdown ? (
<div className="task-document-content-markdown"> <div className="task-document-content-markdown">
<div className="markdown-body"> <div className="markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{expandedContent}</ReactMarkdown> <ReactMarkdown remarkPlugins={[remarkGfm]}>{displayedContent}</ReactMarkdown>
</div> </div>
</div> </div>
) : ( ) : (
<pre className="task-document-content-text">{expandedContent}</pre> <pre className="task-document-content-text">{displayedContent}</pre>
)} )}
</div> </div>
@@ -525,7 +581,7 @@ export function TaskDocumentsTab({
<div <div
key={revision.id} key={revision.id}
className="task-document-revision-item" className="task-document-revision-item"
onClick={() => handleViewRevision(revision)} onClick={() => handleViewRevision(doc.key, revision)}
> >
<div className="revision-header"> <div className="revision-header">
<span className="revision-badge">v{revision.revision}</span> <span className="revision-badge">v{revision.revision}</span>
@@ -570,9 +626,9 @@ export function TaskDocumentsTab({
<div className="task-document-actions"> <div className="task-document-actions">
<button <button
className="btn btn-sm" className="btn btn-sm"
onClick={() => void handleExpandDocument(doc)} onClick={() => handleExpandDocument(doc)}
> >
{expandedDocKey === doc.key ? ( {isExpanded ? (
<> <>
<ChevronUp size={14} /> {t("taskDocuments.collapse", "Collapse")} <ChevronUp size={14} /> {t("taskDocuments.collapse", "Collapse")}
</> </>
@@ -583,7 +639,7 @@ export function TaskDocumentsTab({
)} )}
</button> </button>
{expandedDocKey === doc.key && ( {isExpanded && (
<> <>
<button <button
className="btn btn-sm" className="btn btn-sm"
@@ -593,7 +649,7 @@ export function TaskDocumentsTab({
</button> </button>
{canEdit && editingDocKey !== doc.key && ( {canEdit && editingDocKey !== doc.key && (
<button className="btn btn-sm" onClick={handleStartEdit}> <button className="btn btn-sm" onClick={() => handleStartEdit(doc)}>
{t("taskDocuments.edit", "Edit")} {t("taskDocuments.edit", "Edit")}
</button> </button>
)} )}
@@ -630,7 +686,8 @@ export function TaskDocumentsTab({
)} )}
</div> </div>
</div> </div>
))} );
})}
</div> </div>
)} )}

View File

@@ -1,5 +1,9 @@
/*
FNXC:DashboardTests 2026-07-11-00:00:
FN-7833 changed per-task Artifacts-tab document defaults. Keep multi-document coverage here so the component cannot regress to single-expanded or Plain-first rendering while the separate Documents view remains out of scope.
*/
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import type { ArtifactWithTask, TaskDocument } from "@fusion/core"; import type { ArtifactWithTask, TaskDocument } from "@fusion/core";
import { TaskDocumentsTab } from "../TaskDocumentsTab"; import { TaskDocumentsTab } from "../TaskDocumentsTab";
import { artifactMediaUrl, fetchTaskDocuments, fetchTaskDocumentRevisions } from "../../api"; import { artifactMediaUrl, fetchTaskDocuments, fetchTaskDocumentRevisions } from "../../api";
@@ -23,6 +27,12 @@ const mockFetchTaskDocumentRevisions = vi.mocked(fetchTaskDocumentRevisions);
const mockArtifactMediaUrl = vi.mocked(artifactMediaUrl); const mockArtifactMediaUrl = vi.mocked(artifactMediaUrl);
const mockUseArtifacts = vi.mocked(useArtifacts); const mockUseArtifacts = vi.mocked(useArtifacts);
function getDocumentCard(key: string): HTMLElement {
const keyElement = screen.getAllByText(key).find((element) => element.classList.contains("task-document-key"));
expect(keyElement).toBeDefined();
return keyElement!.closest(".task-document-card") as HTMLElement;
}
const mockArtifacts: ArtifactWithTask[] = [ const mockArtifacts: ArtifactWithTask[] = [
{ {
id: "artifact-image", id: "artifact-image",
@@ -97,6 +107,7 @@ describe("TaskDocumentsTab", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
window.localStorage.clear();
mockFetchTaskDocuments.mockResolvedValue(mockDocuments); mockFetchTaskDocuments.mockResolvedValue(mockDocuments);
mockFetchTaskDocumentRevisions.mockResolvedValue([]); mockFetchTaskDocumentRevisions.mockResolvedValue([]);
mockArtifactMediaUrl.mockImplementation((id: string) => `/api/artifacts/${id}/media`); mockArtifactMediaUrl.mockImplementation((id: string) => `/api/artifacts/${id}/media`);
@@ -113,7 +124,7 @@ describe("TaskDocumentsTab", () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole("heading", { name: "Artifacts" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Artifacts" })).toBeInTheDocument();
expect(screen.getByText("plan")).toBeInTheDocument(); expect(screen.getAllByText("plan").length).toBeGreaterThan(0);
}); });
expect(screen.getByText("notes")).toBeInTheDocument(); expect(screen.getByText("notes")).toBeInTheDocument();
@@ -148,7 +159,7 @@ describe("TaskDocumentsTab", () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("plan")).toBeInTheDocument(); expect(screen.getAllByText("plan").length).toBeGreaterThan(0);
}); });
expect(screen.queryByRole("heading", { name: "Media artifacts" })).not.toBeInTheDocument(); expect(screen.queryByRole("heading", { name: "Media artifacts" })).not.toBeInTheDocument();
@@ -186,7 +197,7 @@ describe("TaskDocumentsTab", () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} projectId="project-1" />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} projectId="project-1" />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("plan")).toBeInTheDocument(); expect(screen.getAllByText("plan").length).toBeGreaterThan(0);
expect(screen.getByRole("heading", { name: "Media artifacts" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Media artifacts" })).toBeInTheDocument();
}); });
@@ -272,7 +283,7 @@ describe("TaskDocumentsTab", () => {
fireEvent.keyDown(document, { key: "Tab", shiftKey: true }); fireEvent.keyDown(document, { key: "Tab", shiftKey: true });
expect(closeButton).toHaveFocus(); expect(closeButton).toHaveFocus();
screen.getAllByRole("button", { name: "Expand" })[0].focus(); screen.getAllByRole("button", { name: "Collapse" })[0].focus();
fireEvent.keyDown(document, { key: "Tab" }); fireEvent.keyDown(document, { key: "Tab" });
expect(closeButton).toHaveFocus(); expect(closeButton).toHaveFocus();
}); });
@@ -292,212 +303,78 @@ describe("TaskDocumentsTab", () => {
}); });
}); });
it("expands document to show content", async () => { it("renders every task document expanded as markdown by default", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { const planStrong = (await screen.findAllByText("plan")).find((element) => element.tagName === "STRONG");
expect(screen.getByText("plan")).toBeInTheDocument(); expect(planStrong).toBeDefined();
}); expect(screen.getByRole("heading", { name: "Notes" })).toBeInTheDocument();
expect(screen.getByText("Item 1")).toBeInTheDocument();
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0]; expect(screen.getByText("Item 2")).toBeInTheDocument();
fireEvent.click(expandButton); expect(document.querySelectorAll(".task-document-content-markdown .markdown-body")).toHaveLength(2);
expect(document.querySelector("pre.task-document-content-text")).not.toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/This is the \*\*plan\*\* content/)).toBeInTheDocument();
});
}); });
it("collapses document when expand button clicked again", async () => { it("collapses only the selected document while other documents stay expanded", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { await screen.findByText("Item 1");
expect(screen.getByText("plan")).toBeInTheDocument(); const planCard = getDocumentCard("plan");
});
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0]; fireEvent.click(within(planCard).getByRole("button", { name: /collapse/i }));
fireEvent.click(expandButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/This is the \*\*plan\*\* content/)).toBeInTheDocument(); expect(screen.queryByText("This is the")).not.toBeInTheDocument();
}); });
expect(screen.getByRole("heading", { name: "Notes" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /collapse/i })); expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(within(planCard).getByRole("button", { name: /expand/i })).toBeInTheDocument();
// Content should be hidden
expect(screen.queryByText(/This is the/)).not.toBeInTheDocument();
}); });
describe("markdown toggle", () => { describe("markdown toggle", () => {
it("defaults to raw text mode", async () => { it("defaults to markdown render mode", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("plan")).toBeInTheDocument(); expect(document.querySelector(".task-document-content-markdown strong")?.textContent).toBe("plan");
});
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0];
fireEvent.click(expandButton);
await waitFor(() => {
// Should show raw markdown syntax
expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument();
}); });
expect(screen.getAllByRole("button", { name: /switch to plain text/i })[0]).toHaveAttribute("aria-pressed", "true");
expect(screen.queryByText(/\*\*plan\*\*/)).not.toBeInTheDocument();
}); });
it("toggles to markdown render mode", async () => { it("toggles to raw text mode", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { const toggleBtn = (await screen.findAllByRole("button", { name: /switch to plain text/i }))[0];
expect(screen.getByText("plan")).toBeInTheDocument();
});
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0];
fireEvent.click(expandButton);
await waitFor(() => {
expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument();
});
// Find and click the markdown toggle
const toggleBtn = screen.getByRole("button", { name: /switch to markdown/i });
expect(toggleBtn).toHaveAttribute("aria-pressed", "false");
fireEvent.click(toggleBtn); fireEvent.click(toggleBtn);
// Should now be in markdown mode
expect(screen.getByRole("button", { name: /switch to plain text/i })).toHaveAttribute("aria-pressed", "true");
// Bold text should be rendered as <strong> - use a data-testid to scope the query
const container = document.querySelector(".task-document-content-markdown");
expect(container).not.toBeNull();
const strongEl = container!.querySelector("strong");
expect(strongEl).not.toBeNull();
expect(strongEl!.textContent).toBe("plan");
});
it("toggles back to raw text mode", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("plan")).toBeInTheDocument(); expect(document.querySelectorAll("pre.task-document-content-text")).toHaveLength(2);
}); });
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0];
fireEvent.click(expandButton);
await waitFor(() => {
expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument();
});
// Toggle to markdown mode
fireEvent.click(screen.getByRole("button", { name: /switch to markdown/i }));
await waitFor(() => {
expect(document.querySelector(".task-document-content-markdown strong")).not.toBeNull();
});
// Toggle back to raw text
fireEvent.click(screen.getByRole("button", { name: /switch to plain text/i }));
// Should be back to raw text mode
expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument(); expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument();
expect(screen.getByText(/# Notes/)).toBeInTheDocument();
expect(document.querySelector(".task-document-content-markdown")).not.toBeInTheDocument();
}); });
it("resets markdown mode when switching documents", async () => { it("persists raw text preference across collapse and re-expand", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => { fireEvent.click((await screen.findAllByRole("button", { name: /switch to plain text/i }))[0]);
expect(screen.getByText("plan")).toBeInTheDocument(); await waitFor(() => expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument());
});
// Expand first document and enable markdown mode const planCard = getDocumentCard("plan");
const expandButtons = screen.getAllByRole("button", { name: /expand/i }); fireEvent.click(within(planCard).getByRole("button", { name: /collapse/i }));
fireEvent.click(expandButtons[0]); fireEvent.click(within(planCard).getByRole("button", { name: /expand/i }));
await waitFor(() => { expect(await screen.findByText(/\*\*plan\*\*/)).toBeInTheDocument();
expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument(); expect(within(planCard).getByRole("button", { name: /switch to markdown/i })).toHaveAttribute("aria-pressed", "false");
});
fireEvent.click(screen.getByRole("button", { name: /switch to markdown/i }));
await waitFor(() => {
expect(document.querySelector(".task-document-content-markdown strong")).not.toBeNull();
});
// Collapse first document
fireEvent.click(screen.getByRole("button", { name: /collapse/i }));
// Expand second document
fireEvent.click(expandButtons[1]);
await waitFor(() => {
// Should be in raw text mode by default
expect(screen.getByText(/# Notes/)).toBeInTheDocument();
});
// Toggle button should be in raw text mode
const toggleBtn = screen.getByRole("button", { name: /switch to markdown/i });
expect(toggleBtn).toHaveAttribute("aria-pressed", "false");
}); });
it("resets markdown mode when collapsing document", async () => { it("renders markdown list syntax without an extra toggle click", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />); render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => {
expect(screen.getByText("plan")).toBeInTheDocument();
});
// Expand document and enable markdown mode
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0];
fireEvent.click(expandButton);
await waitFor(() => {
expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: /switch to markdown/i }));
await waitFor(() => {
expect(document.querySelector(".task-document-content-markdown strong")).not.toBeNull();
});
// Collapse document
fireEvent.click(screen.getByRole("button", { name: /collapse/i }));
// Re-expand document (first expand button after collapse)
fireEvent.click(screen.getAllByRole("button", { name: /expand/i })[0]);
await waitFor(() => {
// Should be in raw text mode by default
expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument();
});
const toggleBtn = screen.getByRole("button", { name: /switch to markdown/i });
expect(toggleBtn).toHaveAttribute("aria-pressed", "false");
});
it("renders markdown list syntax", async () => {
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
await waitFor(() => {
expect(screen.getByText("notes")).toBeInTheDocument();
});
// Expand the notes document (second one)
const expandButtons = screen.getAllByRole("button", { name: /expand/i });
fireEvent.click(expandButtons[1]);
await waitFor(() => {
expect(screen.getByText(/# Notes/)).toBeInTheDocument();
});
// Toggle to markdown mode
fireEvent.click(screen.getByRole("button", { name: /switch to markdown/i }));
// Check heading is rendered
const heading = await screen.findByRole("heading", { name: "Notes" }); const heading = await screen.findByRole("heading", { name: "Notes" });
expect(heading).toBeInTheDocument(); expect(heading).toBeInTheDocument();
// Check list items are rendered
expect(screen.getByText("Item 1")).toBeInTheDocument(); expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument(); expect(screen.getByText("Item 2")).toBeInTheDocument();
}); });