From caeff4bf800f3880da07d4b672b726cfd91d74e2 Mon Sep 17 00:00:00 2001 From: Molecule AI App-FE Date: Mon, 11 May 2026 22:25:08 +0000 Subject: [PATCH] test(canvas/FilesTab): add NotAvailablePanel + FilesToolbar coverage (22 cases) NotAvailablePanel: renders heading, runtime name in monospace, Chat hint, SVG aria-hidden, flex layout. FilesToolbar: directory selector options + aria-label, setRoot on change, file count display, New/Upload/Clear visible only for /configs, Export/Refresh always visible, aria-labels on all buttons, onNewFile/onDownloadAll/onClearAll/onRefresh called on click, focus-visible ring on all buttons. Co-Authored-By: Claude Opus 4.7 --- .../tabs/FilesTab/__tests__/FilesTab.test.tsx | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 canvas/src/components/tabs/FilesTab/__tests__/FilesTab.test.tsx diff --git a/canvas/src/components/tabs/FilesTab/__tests__/FilesTab.test.tsx b/canvas/src/components/tabs/FilesTab/__tests__/FilesTab.test.tsx new file mode 100644 index 00000000..751954e2 --- /dev/null +++ b/canvas/src/components/tabs/FilesTab/__tests__/FilesTab.test.tsx @@ -0,0 +1,224 @@ +// @vitest-environment jsdom +/** + * FilesTab: NotAvailablePanel + FilesToolbar coverage. + * + * NotAvailablePanel: pure presentational component — renders a "feature not + * available" placeholder for external-runtime workspaces. + * FilesToolbar: pure props-driven component — directory selector, file count, + * action buttons (New, Upload, Export, Clear, Refresh) with correct aria-labels. + * + * No @testing-library/jest-dom import — use textContent / className / + * getAttribute checks to avoid "expect is not defined" errors. + */ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import React from "react"; + +import { FilesToolbar } from "../FilesToolbar"; +import { NotAvailablePanel } from "../NotAvailablePanel"; + +// ─── afterEach ───────────────────────────────────────────────────────────────── + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +// ─── NotAvailablePanel ───────────────────────────────────────────────────────── + +describe("NotAvailablePanel", () => { + it("renders heading 'Files not available'", () => { + const { container } = render(); + expect(container.textContent).toContain("Files not available"); + }); + + it("renders the runtime name in monospace", () => { + const { container } = render(); + expect(container.textContent).toContain("external"); + const spans = container.querySelectorAll("span"); + const monoSpans = Array.from(spans).filter( + (s) => s.className && s.className.includes("font-mono"), + ); + expect(monoSpans.length).toBeGreaterThan(0); + }); + + it("renders a Chat tab hint in description", () => { + const { container } = render(); + expect(container.textContent).toContain("Chat tab"); + }); + + it("SVG icon has aria-hidden=true", () => { + const { container } = render(); + const svg = container.querySelector("svg"); + expect(svg?.getAttribute("aria-hidden")).toBe("true"); + }); + + it("renders without crashing for any runtime string", () => { + const { container } = render(); + expect(container.textContent).toContain("unknown-runtime"); + }); + + it("applies the correct layout classes to root div", () => { + const { container } = render(); + const root = container.firstElementChild as HTMLElement; + expect(root.className).toContain("flex"); + expect(root.className).toContain("flex-col"); + expect(root.className).toContain("items-center"); + }); +}); + +// ─── FilesToolbar ─────────────────────────────────────────────────────────────── + +describe("FilesToolbar", () => { + const noop = vi.fn(); + + function renderToolbar(props: Partial> = {}) { + return render( + , + ); + } + + it("renders the directory selector with correct aria-label", () => { + const { container } = renderToolbar(); + const select = container.querySelector("select"); + expect(select?.getAttribute("aria-label")).toBe("File root directory"); + }); + + it("directory selector has all four options", () => { + const { container } = renderToolbar(); + const select = container.querySelector("select") as HTMLSelectElement; + const options = Array.from(select?.options ?? []); + const values = options.map((o) => o.value); + expect(values).toContain("/configs"); + expect(values).toContain("/home"); + expect(values).toContain("/workspace"); + expect(values).toContain("/plugins"); + }); + + it("calls setRoot when directory changes", () => { + const setRoot = vi.fn(); + const { container } = renderToolbar({ setRoot }); + const select = container.querySelector("select") as HTMLSelectElement; + select.value = "/home"; + select.dispatchEvent(new Event("change", { bubbles: true })); + expect(setRoot).toHaveBeenCalledWith("/home"); + }); + + it("displays the file count", () => { + const { container } = renderToolbar({ fileCount: 42 }); + expect(container.textContent).toContain("42 files"); + }); + + it("shows New + Upload + Clear buttons for /configs", () => { + const { container } = renderToolbar({ root: "/configs" }); + const texts = Array.from(container.querySelectorAll("button")).map( + (b) => b.textContent?.trim(), + ); + expect(texts).toContain("+ New"); + expect(texts).toContain("Upload"); + expect(texts).toContain("Clear"); + expect(texts).toContain("Export"); + expect(texts).toContain("↻"); + }); + + it("hides New + Upload + Clear for /workspace", () => { + const { container } = renderToolbar({ root: "/workspace" }); + const texts = Array.from(container.querySelectorAll("button")).map( + (b) => b.textContent?.trim(), + ); + expect(texts).not.toContain("+ New"); + expect(texts).not.toContain("Upload"); + expect(texts).not.toContain("Clear"); + expect(texts).toContain("Export"); + }); + + it("hides New + Upload + Clear for /home", () => { + const { container } = renderToolbar({ root: "/home" }); + const texts = Array.from(container.querySelectorAll("button")).map( + (b) => b.textContent?.trim(), + ); + expect(texts).not.toContain("+ New"); + expect(texts).not.toContain("Upload"); + expect(texts).not.toContain("Clear"); + }); + + it("hides New + Upload + Clear for /plugins", () => { + const { container } = renderToolbar({ root: "/plugins" }); + const texts = Array.from(container.querySelectorAll("button")).map( + (b) => b.textContent?.trim(), + ); + expect(texts).not.toContain("+ New"); + expect(texts).not.toContain("Upload"); + expect(texts).not.toContain("Clear"); + }); + + it("New button has correct aria-label", () => { + const { container } = renderToolbar({ root: "/configs" }); + const newBtn = container.querySelector('button[aria-label="Create new file"]'); + expect(newBtn?.textContent?.trim()).toBe("+ New"); + }); + + it("Export button has correct aria-label", () => { + const { container } = renderToolbar(); + const exportBtn = container.querySelector('button[aria-label="Download all files"]'); + expect(exportBtn?.textContent?.trim()).toBe("Export"); + }); + + it("Clear button has correct aria-label", () => { + const { container } = renderToolbar({ root: "/configs" }); + const clearBtn = container.querySelector('button[aria-label="Delete all files"]'); + expect(clearBtn?.textContent?.trim()).toBe("Clear"); + }); + + it("Refresh button has correct aria-label", () => { + const { container } = renderToolbar(); + const refreshBtn = container.querySelector('button[aria-label="Refresh file list"]'); + expect(refreshBtn?.textContent?.trim()).toBe("↻"); + }); + + it("calls onNewFile when New button is clicked", () => { + const onNewFile = vi.fn(); + const { container } = renderToolbar({ root: "/configs", onNewFile }); + container.querySelector('button[aria-label="Create new file"]')!.click(); + expect(onNewFile).toHaveBeenCalledTimes(1); + }); + + it("calls onDownloadAll when Export button is clicked", () => { + const onDownloadAll = vi.fn(); + const { container } = renderToolbar({ onDownloadAll }); + container.querySelector('button[aria-label="Download all files"]')!.click(); + expect(onDownloadAll).toHaveBeenCalledTimes(1); + }); + + it("calls onClearAll when Clear button is clicked", () => { + const onClearAll = vi.fn(); + const { container } = renderToolbar({ root: "/configs", onClearAll }); + container.querySelector('button[aria-label="Delete all files"]')!.click(); + expect(onClearAll).toHaveBeenCalledTimes(1); + }); + + it("calls onRefresh when Refresh button is clicked", () => { + const onRefresh = vi.fn(); + const { container } = renderToolbar({ onRefresh }); + container.querySelector('button[aria-label="Refresh file list"]')!.click(); + expect(onRefresh).toHaveBeenCalledTimes(1); + }); + + it("applies focus-visible ring to all interactive buttons", () => { + const { container } = renderToolbar({ root: "/configs" }); + const buttons = container.querySelectorAll("button"); + for (const btn of buttons) { + expect(btn.className).toContain("focus-visible:ring-2"); + } + }); +});