diff --git a/canvas/src/components/tabs/FilesTab/__tests__/FilesToolbar.test.tsx b/canvas/src/components/tabs/FilesTab/__tests__/FilesToolbar.test.tsx
new file mode 100644
index 00000000..f749a9f5
--- /dev/null
+++ b/canvas/src/components/tabs/FilesTab/__tests__/FilesToolbar.test.tsx
@@ -0,0 +1,158 @@
+// @vitest-environment jsdom
+/**
+ * Tests for FilesToolbar — file browser toolbar in FilesTab.
+ *
+ * Coverage:
+ * - Renders directory selector (4 options)
+ * - Shows file count
+ * - Shows + New button only for /configs
+ * - Shows upload folder button only for /configs
+ * - Hides + New/upload for /home, /workspace, /plugins
+ * - Shows Download All and Clear All buttons
+ * - Shows Refresh button
+ * - Calls setRoot when directory changes
+ * - Calls onNewFile when + New clicked
+ * - File count updates with prop changes
+ * - Upload input triggers onUpload callback
+ */
+import React from "react";
+import { render, screen, fireEvent, cleanup } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { FilesToolbar } from "../FilesToolbar";
+
+const fireUpload = () => {
+ const input = screen.getByRole("button", { name: /upload folder/i }).closest("div")?.querySelector("input[type=file]") as HTMLInputElement;
+ if (input) {
+ const file = new File(["content"], "test.txt", { type: "text/plain" });
+ Object.defineProperty(input, "files", { value: [file], configurable: true });
+ fireEvent.change(input);
+ }
+};
+
+describe("FilesToolbar", () => {
+ beforeEach(() => { vi.useRealTimers(); });
+ afterEach(() => { cleanup(); vi.useRealTimers(); });
+
+ it("renders directory selector with 4 options", () => {
+ const setRoot = vi.fn();
+ render();
+ expect(screen.getByRole("combobox", { name: /file root directory/i })).toBeTruthy();
+ expect(screen.getByRole("option", { name: "/configs" })).toBeTruthy();
+ expect(screen.getByRole("option", { name: "/home" })).toBeTruthy();
+ expect(screen.getByRole("option", { name: "/workspace" })).toBeTruthy();
+ expect(screen.getByRole("option", { name: "/plugins" })).toBeTruthy();
+ });
+
+ it("shows file count", () => {
+ const setRoot = vi.fn();
+ render();
+ expect(screen.getByText("42 files")).toBeTruthy();
+ });
+
+ it("calls setRoot when directory changes", () => {
+ const setRoot = vi.fn();
+ render();
+ fireEvent.change(screen.getByRole("combobox"), { target: { value: "/workspace" } });
+ expect(setRoot).toHaveBeenCalledWith("/workspace");
+ });
+
+ it("calls onNewFile when + New is clicked", () => {
+ const onNewFile = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /create new file/i }));
+ expect(onNewFile).toHaveBeenCalledTimes(1);
+ });
+
+ it("hides + New button for /home", () => {
+ const onNewFile = vi.fn();
+ render();
+ expect(screen.queryByRole("button", { name: /create new file/i })).toBeFalsy();
+ });
+
+ it("hides + New button for /workspace", () => {
+ render();
+ expect(screen.queryByRole("button", { name: /create new file/i })).toBeFalsy();
+ });
+
+ it("hides + New button for /plugins", () => {
+ render();
+ expect(screen.queryByRole("button", { name: /create new file/i })).toBeFalsy();
+ });
+
+ it("shows upload folder button for /configs", () => {
+ render();
+ expect(screen.getByRole("button", { name: /upload folder/i })).toBeTruthy();
+ });
+
+ it("hides upload folder button for /home", () => {
+ render();
+ expect(screen.queryByRole("button", { name: /upload folder/i })).toBeFalsy();
+ });
+
+ it("calls onUpload when file input changes", () => {
+ const onUpload = vi.fn();
+ render();
+ // The upload button opens a hidden file input. Trigger it via change.
+ const input = document.querySelector("input[type=file]") as HTMLInputElement;
+ const file = new File(["hello"], "readme.txt", { type: "text/plain" });
+ Object.defineProperty(input, "files", { value: [file], configurable: true });
+ fireEvent.change(input);
+ expect(onUpload).toHaveBeenCalledTimes(1);
+ });
+
+ it("shows Export button", () => {
+ const onDownloadAll = vi.fn();
+ render();
+ expect(screen.getByRole("button", { name: /download all files/i })).toBeTruthy();
+ });
+
+ it("calls onDownloadAll when Export clicked", () => {
+ const onDownloadAll = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /download all files/i }));
+ expect(onDownloadAll).toHaveBeenCalledTimes(1);
+ });
+
+ it("shows Clear button for /configs", () => {
+ const onClearAll = vi.fn();
+ render();
+ expect(screen.getByRole("button", { name: /delete all files/i })).toBeTruthy();
+ });
+
+ it("calls onClearAll when Clear clicked", () => {
+ const onClearAll = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /delete all files/i }));
+ expect(onClearAll).toHaveBeenCalledTimes(1);
+ });
+
+ it("shows Refresh button", () => {
+ const onRefresh = vi.fn();
+ render();
+ expect(screen.getByRole("button", { name: /refresh file list/i })).toBeTruthy();
+ });
+
+ it("calls onRefresh when Refresh clicked", () => {
+ const onRefresh = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /refresh file list/i }));
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+ });
+
+ it("file count updates with prop", () => {
+ const { rerender } = render(
+
+ );
+ expect(screen.getByText("5 files")).toBeTruthy();
+ rerender(
+
+ );
+ expect(screen.getByText("99 files")).toBeTruthy();
+ });
+
+ it("selected directory matches root prop", () => {
+ const setRoot = vi.fn();
+ render();
+ expect((screen.getByRole("combobox") as HTMLSelectElement).value).toBe("/plugins");
+ });
+});