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:
@@ -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.
|
||||
@@ -22,6 +22,27 @@ import { ArtifactMedia, getArtifactTypeLabel } from "./ArtifactMedia";
|
||||
// Document key validation: alphanumeric, hyphens, underscores, 1-64 chars
|
||||
const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
|
||||
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 {
|
||||
taskId: string;
|
||||
@@ -109,8 +130,8 @@ export function TaskDocumentsTab({
|
||||
const { t } = useTranslation("app");
|
||||
const [documents, setDocuments] = useState<TaskDocument[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedDocKey, setExpandedDocKey] = useState<string | null>(null);
|
||||
const [expandedContent, setExpandedContent] = useState("");
|
||||
const [expandedDocKeys, setExpandedDocKeys] = useState<Set<string>>(() => new Set());
|
||||
const [revisionContentByKey, setRevisionContentByKey] = useState<Record<string, string>>({});
|
||||
const [editingDocKey, setEditingDocKey] = useState<string | null>(null);
|
||||
const [editContent, setEditContent] = useState("");
|
||||
const [showHistory, setShowHistory] = useState<string | null>(null);
|
||||
@@ -122,7 +143,11 @@ export function TaskDocumentsTab({
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deletingKey, setDeletingKey] = 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:
|
||||
* 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 lightboxCloseRef = useRef<HTMLButtonElement>(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 loadDocuments = useCallback(async () => {
|
||||
try {
|
||||
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);
|
||||
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) {
|
||||
addToast(getErrorMessage(error) || t("taskDocuments.failedToLoad", "Failed to load documents"), "error");
|
||||
} finally {
|
||||
@@ -154,23 +195,30 @@ export function TaskDocumentsTab({
|
||||
}
|
||||
}, [addToast, artifactsError, t]);
|
||||
|
||||
async function handleExpandDocument(doc: TaskDocument) {
|
||||
if (expandedDocKey === doc.key) {
|
||||
setExpandedDocKey(null);
|
||||
setExpandedContent("");
|
||||
setEditingDocKey(null);
|
||||
setEditContent("");
|
||||
setShowHistory(null);
|
||||
setRevisions([]);
|
||||
setRenderMarkdown(false);
|
||||
} else {
|
||||
setExpandedDocKey(doc.key);
|
||||
setExpandedContent(doc.content);
|
||||
setEditingDocKey(null);
|
||||
setEditContent("");
|
||||
setShowHistory(null);
|
||||
setRevisions([]);
|
||||
setRenderMarkdown(false);
|
||||
useEffect(() => {
|
||||
writeBooleanPref(TASK_DOCUMENTS_MARKDOWN_TOGGLE_STORAGE_KEY, renderMarkdown);
|
||||
}, [renderMarkdown]);
|
||||
|
||||
function handleExpandDocument(doc: TaskDocument) {
|
||||
const isExpanded = expandedDocKeys.has(doc.key);
|
||||
setExpandedDocKeys((current) => {
|
||||
const next = new Set(current);
|
||||
if (isExpanded) {
|
||||
next.delete(doc.key);
|
||||
} else {
|
||||
next.add(doc.key);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
if (isExpanded) {
|
||||
if (editingDocKey === doc.key) {
|
||||
setEditingDocKey(null);
|
||||
setEditContent("");
|
||||
}
|
||||
if (showHistory === doc.key) {
|
||||
setShowHistory(null);
|
||||
setRevisions([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,11 +240,9 @@ export function TaskDocumentsTab({
|
||||
}
|
||||
}
|
||||
|
||||
function handleStartEdit() {
|
||||
if (expandedDocKey) {
|
||||
setEditingDocKey(expandedDocKey);
|
||||
setEditContent(expandedContent);
|
||||
}
|
||||
function handleStartEdit(doc: TaskDocument) {
|
||||
setEditingDocKey(doc.key);
|
||||
setEditContent(revisionContentByKey[doc.key] ?? doc.content);
|
||||
}
|
||||
|
||||
function handleCancelEdit() {
|
||||
@@ -211,12 +257,12 @@ export function TaskDocumentsTab({
|
||||
await putTaskDocument(taskId, editingDocKey, editContent, {}, projectId);
|
||||
setEditingDocKey(null);
|
||||
setEditContent("");
|
||||
setRevisionContentByKey((current) => {
|
||||
const next = { ...current };
|
||||
delete next[editingDocKey];
|
||||
return next;
|
||||
});
|
||||
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");
|
||||
} catch (error) {
|
||||
addToast(getErrorMessage(error) || t("taskDocuments.failedToSave", "Failed to save document"), "error");
|
||||
@@ -264,10 +310,16 @@ export function TaskDocumentsTab({
|
||||
await deleteTaskDocument(taskId, key, projectId);
|
||||
setConfirmDelete(null);
|
||||
setDeletingKey(null);
|
||||
if (expandedDocKey === key) {
|
||||
setExpandedDocKey(null);
|
||||
setExpandedContent("");
|
||||
}
|
||||
setExpandedDocKeys((current) => {
|
||||
const next = new Set(current);
|
||||
next.delete(key);
|
||||
return next;
|
||||
});
|
||||
setRevisionContentByKey((current) => {
|
||||
const next = { ...current };
|
||||
delete next[key];
|
||||
return next;
|
||||
});
|
||||
if (showHistory === key) {
|
||||
setShowHistory(null);
|
||||
setRevisions([]);
|
||||
@@ -281,8 +333,8 @@ export function TaskDocumentsTab({
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewRevision(revision: TaskDocumentRevision) {
|
||||
setExpandedContent(revision.content);
|
||||
function handleViewRevision(docKey: string, revision: TaskDocumentRevision) {
|
||||
setRevisionContentByKey((current) => ({ ...current, [docKey]: revision.content }));
|
||||
setEditingDocKey(null);
|
||||
setEditContent("");
|
||||
}
|
||||
@@ -468,7 +520,11 @@ export function TaskDocumentsTab({
|
||||
)
|
||||
) : (
|
||||
<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 className="task-document-card-header">
|
||||
<div className="task-document-card-title">
|
||||
@@ -483,7 +539,7 @@ export function TaskDocumentsTab({
|
||||
</div>
|
||||
|
||||
{/* Expanded Content View */}
|
||||
{expandedDocKey === doc.key && editingDocKey !== doc.key && (
|
||||
{isExpanded && editingDocKey !== doc.key && (
|
||||
<>
|
||||
<div className="task-document-content-header">
|
||||
<button
|
||||
@@ -500,11 +556,11 @@ export function TaskDocumentsTab({
|
||||
{renderMarkdown ? (
|
||||
<div className="task-document-content-markdown">
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{expandedContent}</ReactMarkdown>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{displayedContent}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<pre className="task-document-content-text">{expandedContent}</pre>
|
||||
<pre className="task-document-content-text">{displayedContent}</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -525,7 +581,7 @@ export function TaskDocumentsTab({
|
||||
<div
|
||||
key={revision.id}
|
||||
className="task-document-revision-item"
|
||||
onClick={() => handleViewRevision(revision)}
|
||||
onClick={() => handleViewRevision(doc.key, revision)}
|
||||
>
|
||||
<div className="revision-header">
|
||||
<span className="revision-badge">v{revision.revision}</span>
|
||||
@@ -570,9 +626,9 @@ export function TaskDocumentsTab({
|
||||
<div className="task-document-actions">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleExpandDocument(doc)}
|
||||
onClick={() => handleExpandDocument(doc)}
|
||||
>
|
||||
{expandedDocKey === doc.key ? (
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<ChevronUp size={14} /> {t("taskDocuments.collapse", "Collapse")}
|
||||
</>
|
||||
@@ -583,7 +639,7 @@ export function TaskDocumentsTab({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedDocKey === doc.key && (
|
||||
{isExpanded && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
@@ -593,7 +649,7 @@ export function TaskDocumentsTab({
|
||||
</button>
|
||||
|
||||
{canEdit && editingDocKey !== doc.key && (
|
||||
<button className="btn btn-sm" onClick={handleStartEdit}>
|
||||
<button className="btn btn-sm" onClick={() => handleStartEdit(doc)}>
|
||||
{t("taskDocuments.edit", "Edit")}
|
||||
</button>
|
||||
)}
|
||||
@@ -630,7 +686,8 @@ export function TaskDocumentsTab({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 { 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 { TaskDocumentsTab } from "../TaskDocumentsTab";
|
||||
import { artifactMediaUrl, fetchTaskDocuments, fetchTaskDocumentRevisions } from "../../api";
|
||||
@@ -23,6 +27,12 @@ const mockFetchTaskDocumentRevisions = vi.mocked(fetchTaskDocumentRevisions);
|
||||
const mockArtifactMediaUrl = vi.mocked(artifactMediaUrl);
|
||||
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[] = [
|
||||
{
|
||||
id: "artifact-image",
|
||||
@@ -97,6 +107,7 @@ describe("TaskDocumentsTab", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
window.localStorage.clear();
|
||||
mockFetchTaskDocuments.mockResolvedValue(mockDocuments);
|
||||
mockFetchTaskDocumentRevisions.mockResolvedValue([]);
|
||||
mockArtifactMediaUrl.mockImplementation((id: string) => `/api/artifacts/${id}/media`);
|
||||
@@ -113,7 +124,7 @@ describe("TaskDocumentsTab", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("heading", { name: "Artifacts" })).toBeInTheDocument();
|
||||
expect(screen.getByText("plan")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("plan").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(screen.getByText("notes")).toBeInTheDocument();
|
||||
@@ -148,7 +159,7 @@ describe("TaskDocumentsTab", () => {
|
||||
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("plan")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("plan").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
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" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("plan")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("plan").length).toBeGreaterThan(0);
|
||||
expect(screen.getByRole("heading", { name: "Media artifacts" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -272,7 +283,7 @@ describe("TaskDocumentsTab", () => {
|
||||
fireEvent.keyDown(document, { key: "Tab", shiftKey: true });
|
||||
expect(closeButton).toHaveFocus();
|
||||
|
||||
screen.getAllByRole("button", { name: "Expand" })[0].focus();
|
||||
screen.getAllByRole("button", { name: "Collapse" })[0].focus();
|
||||
fireEvent.keyDown(document, { key: "Tab" });
|
||||
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} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("plan")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0];
|
||||
fireEvent.click(expandButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/This is the \*\*plan\*\* content/)).toBeInTheDocument();
|
||||
});
|
||||
const planStrong = (await screen.findAllByText("plan")).find((element) => element.tagName === "STRONG");
|
||||
expect(planStrong).toBeDefined();
|
||||
expect(screen.getByRole("heading", { name: "Notes" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
expect(document.querySelectorAll(".task-document-content-markdown .markdown-body")).toHaveLength(2);
|
||||
expect(document.querySelector("pre.task-document-content-text")).not.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} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("plan")).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText("Item 1");
|
||||
const planCard = getDocumentCard("plan");
|
||||
|
||||
const expandButton = screen.getAllByRole("button", { name: /expand/i })[0];
|
||||
fireEvent.click(expandButton);
|
||||
fireEvent.click(within(planCard).getByRole("button", { name: /collapse/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/This is the \*\*plan\*\* content/)).toBeInTheDocument();
|
||||
expect(screen.queryByText("This is the")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /collapse/i }));
|
||||
|
||||
// Content should be hidden
|
||||
expect(screen.queryByText(/This is the/)).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Notes" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(within(planCard).getByRole("button", { name: /expand/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("markdown toggle", () => {
|
||||
it("defaults to raw text mode", async () => {
|
||||
it("defaults to markdown render mode", async () => {
|
||||
render(<TaskDocumentsTab taskId="KB-001" addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("plan")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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(document.querySelector(".task-document-content-markdown strong")?.textContent).toBe("plan");
|
||||
});
|
||||
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} />);
|
||||
|
||||
await waitFor(() => {
|
||||
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");
|
||||
|
||||
const toggleBtn = (await screen.findAllByRole("button", { name: /switch to plain text/i }))[0];
|
||||
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(() => {
|
||||
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(/# 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} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("plan")).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click((await screen.findAllByRole("button", { name: /switch to plain text/i }))[0]);
|
||||
await waitFor(() => expect(screen.getByText(/\*\*plan\*\*/)).toBeInTheDocument());
|
||||
|
||||
// Expand first document and enable markdown mode
|
||||
const expandButtons = screen.getAllByRole("button", { name: /expand/i });
|
||||
fireEvent.click(expandButtons[0]);
|
||||
const planCard = getDocumentCard("plan");
|
||||
fireEvent.click(within(planCard).getByRole("button", { name: /collapse/i }));
|
||||
fireEvent.click(within(planCard).getByRole("button", { name: /expand/i }));
|
||||
|
||||
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 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");
|
||||
expect(await screen.findByText(/\*\*plan\*\*/)).toBeInTheDocument();
|
||||
expect(within(planCard).getByRole("button", { name: /switch to markdown/i })).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} />);
|
||||
|
||||
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" });
|
||||
expect(heading).toBeInTheDocument();
|
||||
|
||||
// Check list items are rendered
|
||||
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Item 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user