diff --git a/.changeset/artifact-pipeline-and-gallery.md b/.changeset/artifact-pipeline-and-gallery.md index 0c580e774b..4e73008f5c 100644 --- a/.changeset/artifact-pipeline-and-gallery.md +++ b/.changeset/artifact-pipeline-and-gallery.md @@ -4,4 +4,4 @@ summary: Agents save screenshots, videos, HTML mockups, and PDFs as artifacts, shown in a new category gallery with doc editing. category: feature -dev: fn_artifact_register gains a `path` payload source (file copied into managed storage, MIME inference, image/video/PDF signature validation) and is now always exposed to executor sessions (previously missing in ephemeral mode) with worktree-relative path resolution and executing-task default taskId; executor/planning prompts instruct agents to register visual/media deliverables (images, videos, HTML mockups, PDFs); the media route serves HTTP byte ranges for video/audio seeking; video attachments (100MB cap) bridge into the registry like images; HTML docs render as live sandboxed previews; new `GET`/`PATCH /api/artifacts/:id` routes plus `TaskStore.updateArtifact` and the `artifact:updated` SSE event power in-place doc editing in the new ArtifactsGallery (Images/Docs/PDFs/Videos/Audio/Other sections with per-category viewers, mobile-responsive). +dev: fn_artifact_register gains a `path` payload source (file copied into managed storage, MIME inference, image/video/PDF signature validation) and is now always exposed to executor sessions (previously missing in ephemeral mode) with worktree-relative path resolution and executing-task default taskId; executor/planning prompts instruct agents to register visual/media deliverables (images, videos, HTML mockups, PDFs); the media route serves HTTP byte ranges for video/audio seeking; video attachments (100MB cap) bridge into the registry like images; HTML docs render as live sandboxed previews; new `GET`/`PATCH /api/artifacts/:id` routes plus `TaskStore.updateArtifact` and the `artifact:updated` SSE event power in-place doc editing in the new ArtifactsGallery (Images/Docs/PDFs/Videos/Audio/Other sections with per-category viewers, mobile-responsive); viewers open in draggable/resizable FloatingWindows, Artifacts is the first/landing tab of the view, and mobile tab buttons render at the uniform 44px control height. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index e63665a301..5a40d07634 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -809,7 +809,7 @@ For per-run aggregation, `GET /api/agents/:id/runs/:runId/cited-goals` returns ` ## Artifacts View -Artifacts view aggregates project markdown files, task documents, and registered artifacts. The dashboard title is **Artifacts**; the internal tab bar keeps the shipped **Project Files**, **Task Documents**, and **Artifacts** labels. +Artifacts view aggregates registered artifacts, project markdown files, and task documents. The dashboard title is **Artifacts**; the internal tab bar leads with **Artifacts** (the landing tab), followed by **Project Files** and **Task Documents**. On mobile the tab buttons render at the uniform 44px control height with non-wrapping labels in a horizontally scrollable row. Features: @@ -824,10 +824,10 @@ Features: - Video artifacts (agent-registered recordings, `path`-ingested MP4/WebM/MOV, and bridged video attachments) play with working seek because the media route serves HTTP byte ranges - HTML doc artifacts (`mimeType: text/html`) render as **live sandboxed previews** by default in the document viewer (scripts allowed, same-origin denied), with a Preview/Source toggle and the same Edit mode as other docs - **Edit any inline-content doc in place**: the document viewer's **Edit** button switches to an editor whose **Save** persists through `PATCH /api/artifacts/:id` and live-refreshes open galleries via the `artifact:updated` registry event; binary-backed documents stay read-only with a media link -- Dismiss any viewer with the close button, backdrop click, or Escape +- Every viewer (image/video lightbox, PDF viewer, document viewer) opens in a draggable, resizable floating window (drag by the viewer header, resize by any edge/corner; geometry persists per viewer kind); dismiss with the close button or Escape. Windows are non-blocking, so the gallery behind them stays interactive - Read artifact metadata on cards, rows, and viewer footers: title, optional description, author ID, timestamp, size, and linked task ID when present - Use the task link on a card/row or viewer footer to jump back to the originating task when the artifact has a `taskId`; inside task detail, the **Artifacts** tab shows that task's documents and registered media artifacts together -- The gallery scales down at the mobile breakpoint (including landscape phones): category chips scroll horizontally, visual grids collapse to two columns, cards and rows go single-column, and viewers become full-screen sheets +- The gallery scales down at the mobile breakpoint (including landscape phones): category chips scroll horizontally, visual grids collapse to two columns, cards and rows go single-column, and viewer windows clamp to the viewport - Loading state: the Artifacts tab shows `Loading artifacts…` while the first artifact list request is pending and no artifact results are loaded - Empty states: with no search query it shows `No artifacts yet.` plus the hint that artifacts are created by agents, users, and system tools; with a search query it shows `No artifacts match "".` - Error state: a failed artifact list request uses the shared `Failed to load artifacts: ` panel with a **Retry** action that re-runs the artifact fetch diff --git a/docs/screenshots/artifacts-doc-viewer.png b/docs/screenshots/artifacts-doc-viewer.png index a7818a0a9f..076eb369df 100644 --- a/docs/screenshots/artifacts-doc-viewer.png +++ b/docs/screenshots/artifacts-doc-viewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6b0fc2cb2711509e64e7ef6ffb3fcba5895ae7169503ed661816e285c486c28 -size 141871 +oid sha256:04f7a1c82605eba16e0900eb2c01d00122e10084d63e71f8e0a5cfa47ca789e2 +size 142244 diff --git a/docs/screenshots/artifacts-mobile.png b/docs/screenshots/artifacts-mobile.png index c1d04a37b0..75a4261fde 100644 --- a/docs/screenshots/artifacts-mobile.png +++ b/docs/screenshots/artifacts-mobile.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b350855247627f45ec381116e16d01bccd83231cbca3811e96d9cf224e95967 -size 59572 +oid sha256:f0f9ddf5a9194d206874a15421a69207cc80d0e2158ddf7310ae8f9ed28df613 +size 55378 diff --git a/packages/dashboard/app/components/ArtifactsGallery.css b/packages/dashboard/app/components/ArtifactsGallery.css index de785293ed..837ebd5b19 100644 --- a/packages/dashboard/app/components/ArtifactsGallery.css +++ b/packages/dashboard/app/components/ArtifactsGallery.css @@ -351,34 +351,33 @@ grids drop to two columns, cards/rows go single-column, and viewers become full- /* ── Viewers (lightbox / pdf / doc) ────────────────────────────────── */ -.artifacts-gallery-overlay { +/* +FNXC:ArtifactsGallery 2026-07-11-11:30: +Viewers live inside the shared FloatingWindow (draggable by the viewer header, resizable by edge/corner handles), so the viewer fills the window body and its media/doc content flexes with the user-chosen size instead of fixed dvh heights. +*/ +.artifacts-gallery-window .floating-window__body { display: flex; - align-items: center; - justify-content: center; - padding: var(--space-lg); + min-height: 0; } .artifacts-gallery-viewer { display: flex; flex-direction: column; gap: var(--space-sm); - width: min(46rem, 100%); - max-height: 90dvh; + width: 100%; + min-height: 0; padding: var(--space-md); - border: thin solid var(--border); - border-radius: var(--radius-lg); - background: var(--bg); - box-shadow: var(--shadow-lg); -} - -.artifacts-gallery-viewer--wide { - width: min(64rem, 100%); } .artifacts-gallery-viewer-header { display: flex; align-items: center; gap: var(--space-sm); + cursor: grab; +} + +.artifacts-gallery-viewer-header:active { + cursor: grabbing; } .artifacts-gallery-viewer-title { @@ -402,6 +401,7 @@ grids drop to two columns, cards/rows go single-column, and viewers become full- display: flex; align-items: center; justify-content: center; + flex: 1; min-height: 0; overflow: hidden; border-radius: var(--radius-lg); @@ -411,13 +411,14 @@ grids drop to two columns, cards/rows go single-column, and viewers become full- .artifacts-gallery-viewer-media { display: block; max-width: 100%; - max-height: 65dvh; + max-height: 100%; object-fit: contain; } .artifacts-gallery-viewer-pdf { width: 100%; - height: 65dvh; + flex: 1; + min-height: 0; border: thin solid var(--border); border-radius: var(--radius-lg); background: var(--surface); @@ -426,7 +427,8 @@ grids drop to two columns, cards/rows go single-column, and viewers become full- /* HTML mockup live preview; white canvas because agent mockups usually assume a page background. */ .artifacts-gallery-viewer-html { width: 100%; - height: 60dvh; + height: 100%; + min-height: 20rem; border: thin solid var(--border); border-radius: var(--radius-lg); background: #fff; @@ -530,24 +532,8 @@ grids drop to two columns, cards/rows go single-column, and viewers become full- width: 100%; } - .artifacts-gallery-overlay { - padding: 0; - } - - .artifacts-gallery-viewer, - .artifacts-gallery-viewer--wide { - width: 100%; - height: 100dvh; - max-height: none; - border: 0; - border-radius: 0; - } - - .artifacts-gallery-viewer-media { - max-height: 70dvh; - } - - .artifacts-gallery-viewer-pdf { - height: 70dvh; + /* FloatingWindow clamps viewer geometry to the viewport, so mobile only tightens the inner padding. */ + .artifacts-gallery-viewer { + padding: var(--space-sm); } } diff --git a/packages/dashboard/app/components/ArtifactsGallery.tsx b/packages/dashboard/app/components/ArtifactsGallery.tsx index 8f4a332da1..ae16a4f4f0 100644 --- a/packages/dashboard/app/components/ArtifactsGallery.tsx +++ b/packages/dashboard/app/components/ArtifactsGallery.tsx @@ -1,5 +1,5 @@ import "./ArtifactsGallery.css"; -import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent, type MouseEvent } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { @@ -20,6 +20,7 @@ import type { Artifact, ArtifactWithTask } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; import { artifactMediaUrl, fetchArtifact, updateArtifact } from "../api"; import { FileEditor } from "./FileEditor"; +import { FloatingWindow } from "./FloatingWindow"; /* FNXC:ArtifactRegistry 2026-07-10-15:40: @@ -425,8 +426,6 @@ interface OverlayProps { function useOverlayDismiss(onClose: () => void, closeRef: React.RefObject) { useEffect(() => { - const previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; closeRef.current?.focus(); const handleKeyDown = (event: globalThis.KeyboardEvent) => { @@ -438,37 +437,40 @@ function useOverlayDismiss(onClose: () => void, closeRef: React.RefObject { - document.body.style.overflow = previousOverflow; document.removeEventListener("keydown", handleKeyDown); }; }, [closeRef, onClose]); } /* -FNXC:ArtifactsGallery 2026-07-10-15:40: -Each viewer owns its close-button ref and passes it to both OverlayShell (Escape/scroll-lock lifecycle + autofocus so keyboard users land on a dismiss affordance) and ViewerHeader (which renders the actual button). +FNXC:ArtifactsGallery 2026-07-11-11:30: +Artifact viewers host inside the app's canonical FloatingWindow so every popup is DRAGGABLE (by the +viewer header — FloatingWindow ignores pointerdowns on buttons, so header actions stay clickable) and +RESIZABLE (edge/corner handles), with geometry persisted per viewer kind. hideHeader keeps the +viewer's own header chrome (title + actions + close); Escape still dismisses and the close button +autofocuses via useOverlayDismiss. Each viewer owns its close-button ref and passes it to both +OverlayShell and ViewerHeader. */ -function OverlayShell({ label, onClose, children, wide, closeRef }: { label: string; onClose: () => void; children: React.ReactNode; wide?: boolean; closeRef: React.RefObject }) { +function OverlayShell({ label, onClose, children, wide, closeRef, windowKey, persistKey }: { label: string; onClose: () => void; children: React.ReactNode; wide?: boolean; closeRef: React.RefObject; windowKey: string; persistKey: string }) { useOverlayDismiss(onClose, closeRef); - const handleOverlayClick = (event: MouseEvent) => { - if (event.target === event.currentTarget) { - onClose(); - } - }; - return ( -
-
event.stopPropagation()}> +
{children}
-
+ ); } @@ -511,7 +513,7 @@ function MediaLightbox({ artifact, projectId, t, onClose, onOpenTask }: OverlayP const closeRef = useRef(null); return ( - + (null); return ( - + + ("project"); + // FNXC:ArtifactsView 2026-07-11-11:30: Artifacts is the first tab and the landing tab — the view is the artifact gallery first, with project files and task documents as secondary tabs. + const [activeTab, setActiveTab] = useState("artifacts"); const [searchQuery, setSearchQuery] = useState(""); const [selectedFile, setSelectedFile] = useState(null); const [showHiddenProjectFiles, setShowHiddenProjectFiles] = useState(false); @@ -210,7 +211,6 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac const [fileError, setFileError] = useState(null); const [isMobile, setIsMobile] = useState(false); const requestIdRef = useRef(0); - const initialTabSetRef = useRef(false); const markdownPreviewRef = useRef(null); const plainPreviewRef = useRef(null); // Markdown render toggle for project file preview @@ -267,8 +267,7 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac }, []); useEffect(() => { - initialTabSetRef.current = false; - setActiveTab("project"); + setActiveTab("artifacts"); setSelectedFile(null); setShowHiddenProjectFiles(false); setFileContent(null); @@ -278,22 +277,6 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac setTaskDocMarkdownStates(new Map()); }, [projectId]); - useEffect(() => { - if (initialTabSetRef.current || documentsLoading || projectFilesLoading || artifactsLoading) { - return; - } - - if (projectFiles.length > 0) { - setActiveTab("project"); - } else if (documents.length > 0) { - setActiveTab("tasks"); - } else if (artifacts.length > 0) { - setActiveTab("artifacts"); - } - - initialTabSetRef.current = true; - }, [artifacts.length, artifactsLoading, documents.length, documentsLoading, projectFiles.length, projectFilesLoading]); - const groupedDocuments = useMemo(() => { const groups = new Map(); for (const doc of documents) { @@ -481,6 +464,20 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
+ {/* + FNXC:ArtifactRegistry 2026-07-11-11:30: + Artifacts leads the tab bar (and is the landing tab) — the view is the artifact gallery first; project files and task documents are secondary tabs. + */} + - {/* - FNXC:ArtifactRegistry 2026-06-21-04:46: - The Documents navigation has one canonical Artifacts tab so media produced by any agent is discoverable without adding another dashboard destination. - */} -
{activeTab === "project" && ( diff --git a/packages/dashboard/app/components/FloatingWindow.tsx b/packages/dashboard/app/components/FloatingWindow.tsx index 846bb463eb..75ef870b56 100644 --- a/packages/dashboard/app/components/FloatingWindow.tsx +++ b/packages/dashboard/app/components/FloatingWindow.tsx @@ -55,6 +55,8 @@ export interface FloatingWindowProps { closeOnOutsidePointerDown?: boolean; /** Layer band for z-index claiming. Task details stay with the board/task-detail surface; utilities use the global floating stack. */ layer?: "utility" | "task-detail"; + // FNXC:FloatingWindow 2026-07-11-11:30: accessible name for the dialog overlay so headerless windows (e.g. artifact viewers with their own header chrome) stay queryable/announcable by label. + ariaLabel?: string; } const DEFAULT_WIDTH = 720; @@ -158,6 +160,7 @@ export function FloatingWindow({ persistGeometryKey, closeOnOutsidePointerDown = false, layer = "utility", + ariaLabel, }: FloatingWindowProps) { const resolvedMinSize: FloatingWindowSize = minSize ?? { width: DEFAULT_MIN_WIDTH, height: DEFAULT_MIN_HEIGHT }; const initialGeometry = useRef<{ size: FloatingWindowSize; position: FloatingWindowPosition } | null>(null); @@ -411,6 +414,7 @@ export function FloatingWindow({ className="floating-window-overlay" role="dialog" aria-modal="false" + aria-label={ariaLabel} data-testid={`floating-window-overlay-${windowKey}`} // FNXC:FloatingWindow 2026-06-22-23:00: The z-index MUST live on the position:fixed overlay (which creates a stacking context), not the panel. A panel z-index is trapped inside the overlay's context and loses to page elements that are stacking contexts in body's context (e.g. the right dock at position:absolute z-index:20). With z on the overlay, the whole window sits at the shared floating band in body's stacking context and reliably paints above page content + tap-to-front reorders correctly. style={{ zIndex }} diff --git a/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx b/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx index 6ef9493ed9..5c9efb5686 100644 --- a/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx +++ b/packages/dashboard/app/components/__tests__/DocumentsView.test.tsx @@ -300,10 +300,19 @@ describe("DocumentsView", () => { window.innerWidth = originalInnerWidth; }); - it("renders project files tab with markdown file list", () => { + /* + FNXC:ArtifactsView 2026-07-11-11:30: + The view lands on the Artifacts tab (Artifacts-first ordering); Project Files is an explicit click away. + */ + it("lands on the artifacts tab and shows project files after switching tabs", () => { render(); expect(screen.getByRole("heading", { name: "Artifacts" })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /show artifacts/i })).toHaveAttribute("aria-selected", "true"); + const tabs = screen.getAllByRole("tab"); + expect(tabs[0]).toHaveAccessibleName(/show artifacts/i); + + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); expect(screen.getByRole("tab", { name: /show project markdown files/i })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("button", { name: "Open README.md" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Open docs/guide.md" })).toBeInTheDocument(); @@ -322,6 +331,8 @@ describe("DocumentsView", () => { })); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); expect(screen.queryByRole("button", { name: "Open .hidden/notes.md" })).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: /show hidden project files/i })).toHaveAttribute("aria-pressed", "false"); @@ -344,9 +355,8 @@ describe("DocumentsView", () => { render(); - await waitFor(() => { - expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); - }); + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); expect(screen.getByText("KB-001")).toBeInTheDocument(); expect(screen.getByText("KB-002")).toBeInTheDocument(); @@ -379,9 +389,8 @@ describe("DocumentsView", () => { render(); - await waitFor(() => { - expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); - }); + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); const doneGroup = screen.getByRole("button", { name: /expand documents for task KB-DONE/i }).closest(".documents-group"); const todoGroup = screen.getByRole("button", { name: /expand documents for task KB-TODO/i }).closest(".documents-group"); @@ -423,9 +432,8 @@ describe("DocumentsView", () => { render(); - await waitFor(() => { - expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); - }); + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); const doneGroup = screen.getByRole("button", { name: /expand documents for task KB-DONE/i }).closest(".documents-group") as HTMLElement; const status = within(doneGroup).getByLabelText("Task status: Done"); @@ -497,7 +505,7 @@ describe("DocumentsView", () => { expect(onOpenDetail).not.toHaveBeenCalled(); }); - it("opens and dismisses the image and video artifact lightbox by click keyboard close backdrop and escape", () => { + it("opens image and video viewers in a draggable resizable floating window dismissed by close button and escape", () => { mockUseArtifacts.mockReturnValue({ artifacts: mockArtifacts, loading: false, @@ -505,14 +513,20 @@ describe("DocumentsView", () => { refresh: vi.fn().mockResolvedValue(undefined), }); - const { container } = render(); - - fireEvent.click(screen.getByRole("tab", { name: /show artifacts/i })); + render(); + /* + FNXC:ArtifactsGallery 2026-07-11-11:30: + Viewers host in the shared FloatingWindow: draggable (drag/resize handle testids) and resizable, + closed by the close button or Escape. The floating overlay is intentionally click-through + (non-blocking windows), so backdrop-click dismissal and body scroll locking are gone by design. + The FloatingWindow portals to document.body, so media queries use document, not the container. + */ fireEvent.click(screen.getByRole("button", { name: "Expand Image artifact" })); let dialog = screen.getByRole("dialog", { name: "Artifact media preview" }); expect(within(dialog).getByRole("img", { name: "Image artifact" })).toHaveAttribute("src", "/api/artifacts/artifact-image/media"); - expect(document.body.style.overflow).toBe("hidden"); + expect(screen.getByTestId("floating-window-artifact-media-artifact-image")).toBeInTheDocument(); + expect(screen.getByTestId("floating-window-resize-se")).toBeInTheDocument(); fireEvent.click(within(dialog).getByRole("button", { name: "Close artifact preview" })); expect(screen.queryByRole("dialog", { name: "Artifact media preview" })).not.toBeInTheDocument(); @@ -520,16 +534,15 @@ describe("DocumentsView", () => { fireEvent.keyDown(screen.getByRole("button", { name: "Expand Image artifact" }), { key: "Enter" }); dialog = screen.getByRole("dialog", { name: "Artifact media preview" }); expect(within(dialog).getByRole("img", { name: "Image artifact" })).toBeInTheDocument(); - fireEvent.click(dialog); + fireEvent.keyDown(document, { key: "Escape" }); expect(screen.queryByRole("dialog", { name: "Artifact media preview" })).not.toBeInTheDocument(); fireEvent.keyDown(screen.getByRole("button", { name: "Expand Video artifact" }), { key: " " }); dialog = screen.getByRole("dialog", { name: "Artifact media preview" }); expect(within(dialog).getByLabelText("Video artifact: Video artifact").tagName).toBe("VIDEO"); - expect(container.querySelector(".artifacts-gallery-viewer-media-frame video")).toHaveAttribute("controls"); + expect(document.querySelector(".artifacts-gallery-viewer-media-frame video")).toHaveAttribute("controls"); fireEvent.keyDown(document, { key: "Escape" }); expect(screen.queryByRole("dialog", { name: "Artifact media preview" })).not.toBeInTheDocument(); - expect(document.body.style.overflow).toBe(""); }); it("renders artifacts empty loading error retry and mobile gallery states", async () => { @@ -641,15 +654,16 @@ describe("DocumentsView", () => { }); mockFetchArtifact.mockResolvedValue(htmlArtifact); - const { container } = render(); + render(); fireEvent.click(screen.getByRole("tab", { name: /show artifacts/i })); const htmlCard = screen.getByRole("article", { name: "Artifact Login mockup" }); fireEvent.click(within(htmlCard).getByRole("button", { name: "Open Login mockup" })); const dialog = await screen.findByRole("dialog", { name: "Document artifact viewer" }); + // The FloatingWindow portals to document.body, so query the document rather than the render container. await waitFor(() => { - const iframe = container.querySelector(".artifacts-gallery-viewer-html"); + const iframe = document.querySelector(".artifacts-gallery-viewer-html"); expect(iframe).toBeInTheDocument(); expect(iframe).toHaveAttribute("sandbox", "allow-scripts"); expect(iframe).toHaveAttribute("srcdoc", "

Login mock

"); @@ -657,12 +671,14 @@ describe("DocumentsView", () => { // The toggle shows the CURRENT mode (matching the Markdown/Plain convention): "Preview" while previewing. fireEvent.click(within(dialog).getByRole("button", { name: "Preview" })); - expect(container.querySelector(".artifacts-gallery-viewer-html")).not.toBeInTheDocument(); + expect(document.querySelector(".artifacts-gallery-viewer-html")).not.toBeInTheDocument(); expect(within(dialog).getByText("

Login mock

")).toBeInTheDocument(); }); it("clicking project file shows content", async () => { render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); @@ -681,6 +697,8 @@ describe("DocumentsView", () => { */ it("shows a Read-only badge in the project file preview header in plain and markdown modes", async () => { render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); await screen.findByText(/Hello docs/); @@ -695,6 +713,8 @@ describe("DocumentsView", () => { mockSelectionRect(); const onSendSelectionToTask = vi.fn(); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); const plainPreview = await screen.findByText(/Hello docs/); @@ -713,6 +733,8 @@ describe("DocumentsView", () => { mockSelectionRect(); const onSendSelectionToTask = vi.fn(); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); await screen.findByText(/Hello docs/); @@ -756,9 +778,8 @@ describe("DocumentsView", () => { render(); - await waitFor(() => { - expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); - }); + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); fireEvent.change(screen.getByRole("textbox", { name: /search task documents/i }), { target: { value: "alpha" }, @@ -789,6 +810,8 @@ describe("DocumentsView", () => { }); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); expect(screen.getByText(/failed to load project files/i)).toBeInTheDocument(); @@ -816,6 +839,8 @@ describe("DocumentsView", () => { }); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); expect(screen.getByText("Loading project markdown files…")).toBeInTheDocument(); @@ -828,6 +853,8 @@ describe("DocumentsView", () => { window.innerWidth = 600; render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); @@ -849,9 +876,8 @@ describe("DocumentsView", () => { render(); - await waitFor(() => { - expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); - }); + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); fireEvent.click(screen.getByRole("button", { name: /open task KB-001/i })); @@ -865,6 +891,8 @@ describe("DocumentsView", () => { mockFetchWorkspaceFileContent.mockRejectedValue(new Error("cannot read file")); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); @@ -880,6 +908,8 @@ describe("DocumentsView", () => { }); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); @@ -900,6 +930,8 @@ describe("DocumentsView", () => { }); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); @@ -930,6 +962,8 @@ describe("DocumentsView", () => { }); render(); + // Landing tab is now Artifacts; these tests exercise the Project Files tab explicitly. + fireEvent.click(screen.getByRole("tab", { name: /show project markdown files/i })); // Toggle project file to markdown mode fireEvent.click(screen.getByRole("button", { name: "Open README.md" })); @@ -976,9 +1010,8 @@ describe("DocumentsView", () => { render(); - await waitFor(() => { - expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); - }); + fireEvent.click(screen.getByRole("tab", { name: /show task documents/i })); + expect(screen.getByRole("tab", { name: /show task documents/i })).toHaveAttribute("aria-selected", "true"); // Expand task group fireEvent.click(screen.getByRole("button", { name: /expand documents for task KB-001/i }));