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");
+ }
+ });
+});