|
|
|
@@ -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(<NotAvailablePanel runtime="external" />);
|
|
|
|
|
expect(container.textContent).toContain("Files not available");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders the runtime name in monospace", () => {
|
|
|
|
|
const { container } = render(<NotAvailablePanel runtime="external" />);
|
|
|
|
|
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(<NotAvailablePanel runtime="remote-agent" />);
|
|
|
|
|
expect(container.textContent).toContain("Chat tab");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("SVG icon has aria-hidden=true", () => {
|
|
|
|
|
const { container } = render(<NotAvailablePanel runtime="external" />);
|
|
|
|
|
const svg = container.querySelector("svg");
|
|
|
|
|
expect(svg?.getAttribute("aria-hidden")).toBe("true");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders without crashing for any runtime string", () => {
|
|
|
|
|
const { container } = render(<NotAvailablePanel runtime="unknown-runtime" />);
|
|
|
|
|
expect(container.textContent).toContain("unknown-runtime");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("applies the correct layout classes to root div", () => {
|
|
|
|
|
const { container } = render(<NotAvailablePanel runtime="external" />);
|
|
|
|
|
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<React.ComponentProps<typeof FilesToolbar>> = {}) {
|
|
|
|
|
return render(
|
|
|
|
|
<FilesToolbar
|
|
|
|
|
root="/configs"
|
|
|
|
|
setRoot={noop}
|
|
|
|
|
fileCount={0}
|
|
|
|
|
onNewFile={noop}
|
|
|
|
|
onUpload={noop}
|
|
|
|
|
onDownloadAll={noop}
|
|
|
|
|
onClearAll={noop}
|
|
|
|
|
onRefresh={noop}
|
|
|
|
|
{...props}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|