diff --git a/client/package.json b/client/package.json
index 888495c56..06cc72f96 100644
--- a/client/package.json
+++ b/client/package.json
@@ -18,7 +18,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test": "vitest run"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.3",
@@ -40,6 +41,8 @@
},
"devDependencies": {
"@eslint/js": "^9.11.1",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.2.0",
"@types/node": "^22.7.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
@@ -50,10 +53,12 @@
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.9.0",
+ "jsdom": "^26.0.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "^5.5.3",
"typescript-eslint": "^8.7.0",
- "vite": "^5.4.8"
+ "vite": "^5.4.8",
+ "vitest": "^3.0.0"
}
}
diff --git a/client/src/components/ListPane.test.tsx b/client/src/components/ListPane.test.tsx
new file mode 100644
index 000000000..9033dee5b
--- /dev/null
+++ b/client/src/components/ListPane.test.tsx
@@ -0,0 +1,59 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import ListPane from "./ListPane";
+import { describe, it, expect, vi } from "vitest";
+
+describe("ListPane", () => {
+ const defaultProps = {
+ items: [
+ { id: 1, name: "Item 1" },
+ { id: 2, name: "Item 2" },
+ ],
+ listItems: vi.fn(),
+ clearItems: vi.fn(),
+ setSelectedItem: vi.fn(),
+ renderItem: (item: { name: string }) => {item.name},
+ title: "Test List",
+ buttonText: "List Items",
+ };
+
+ it("renders title correctly", () => {
+ render();
+ expect(screen.getByText("Test List")).toBeInTheDocument();
+ });
+
+ it("renders list items using renderItem prop", () => {
+ render();
+ expect(screen.getByText("Item 1")).toBeInTheDocument();
+ expect(screen.getByText("Item 2")).toBeInTheDocument();
+ });
+
+ it("calls listItems when List Items button is clicked", () => {
+ render();
+ fireEvent.click(screen.getByText("List Items"));
+ expect(defaultProps.listItems).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls clearItems when Clear button is clicked", () => {
+ render();
+ fireEvent.click(screen.getByText("Clear"));
+ expect(defaultProps.clearItems).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls setSelectedItem when an item is clicked", () => {
+ render();
+ fireEvent.click(screen.getByText("Item 1"));
+ expect(defaultProps.setSelectedItem).toHaveBeenCalledWith(
+ defaultProps.items[0],
+ );
+ });
+
+ it("disables Clear button when items array is empty", () => {
+ render();
+ expect(screen.getByText("Clear")).toBeDisabled();
+ });
+
+ it("respects isButtonDisabled prop for List Items button", () => {
+ render();
+ expect(screen.getByText("List Items")).toBeDisabled();
+ });
+});
diff --git a/client/src/components/ui/Button.test.tsx b/client/src/components/ui/Button.test.tsx
new file mode 100644
index 000000000..6d7c1cddc
--- /dev/null
+++ b/client/src/components/ui/Button.test.tsx
@@ -0,0 +1,55 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { Button } from "./button";
+import { describe, it, expect, vi } from "vitest";
+import { createRef } from "react";
+
+describe("Button", () => {
+ it("renders children correctly", () => {
+ render();
+ expect(screen.getByText("Click me")).toBeInTheDocument();
+ });
+
+ it("handles click events", () => {
+ const handleClick = vi.fn();
+ render();
+ fireEvent.click(screen.getByText("Click me"));
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+ it("applies different variants correctly", () => {
+ const { rerender } = render();
+ expect(screen.getByText("Default")).toHaveClass("bg-primary");
+
+ rerender();
+ expect(screen.getByText("Outline")).toHaveClass("border-input");
+
+ rerender();
+ expect(screen.getByText("Secondary")).toHaveClass("bg-secondary");
+ });
+
+ it("applies different sizes correctly", () => {
+ const { rerender } = render();
+ expect(screen.getByText("Default")).toHaveClass("h-9");
+
+ rerender();
+ expect(screen.getByText("Small")).toHaveClass("h-8");
+
+ rerender();
+ expect(screen.getByText("Large")).toHaveClass("h-10");
+ });
+
+ it("forwards ref correctly", () => {
+ const ref = createRef();
+ render();
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement);
+ });
+
+ it("renders as a different element when asChild is true", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Link Button").tagName).toBe("A");
+ });
+});
diff --git a/client/src/test.d.ts b/client/src/test.d.ts
new file mode 100644
index 000000000..9f6546258
--- /dev/null
+++ b/client/src/test.d.ts
@@ -0,0 +1,12 @@
+///
+///
+
+import "@testing-library/jest-dom";
+
+declare global {
+ namespace Vi {
+ interface JestAssertion extends jest.Matchers {}
+ }
+}
+
+export {};
diff --git a/client/test/setupTests.ts b/client/test/setupTests.ts
new file mode 100644
index 000000000..b07557686
--- /dev/null
+++ b/client/test/setupTests.ts
@@ -0,0 +1,6 @@
+///
+///
+import "@testing-library/jest-dom/vitest";
+
+// Add any additional test setup, custom matchers, or global mocks here
+// This file runs before each test file
diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json
index 980c215bf..88a07528b 100644
--- a/client/tsconfig.app.json
+++ b/client/tsconfig.app.json
@@ -4,6 +4,7 @@
"paths": {
"@/*": ["./src/*"]
},
+ "types": ["vitest/globals", "@testing-library/jest-dom"],
"target": "ES2020",
"useDefineForClassFields": true,
@@ -26,5 +27,5 @@
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true
},
- "include": ["src"]
+ "include": ["src", "test"]
}
diff --git a/client/tsconfig.json b/client/tsconfig.json
index fec8c8e5c..c31cb2515 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -2,7 +2,8 @@
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
+ { "path": "./tsconfig.node.json" },
+ { "path": "./tsconfig.test.json" }
],
"compilerOptions": {
"baseUrl": ".",
diff --git a/client/tsconfig.test.json b/client/tsconfig.test.json
new file mode 100644
index 000000000..e234462a6
--- /dev/null
+++ b/client/tsconfig.test.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.app.json",
+ "compilerOptions": {
+ "types": ["vitest/globals", "@testing-library/jest-dom"]
+ },
+ "include": ["src/**/*.test.tsx", "src/**/*.test.ts", "test/**/*.ts"]
+}
diff --git a/client/vitest.config.ts b/client/vitest.config.ts
new file mode 100644
index 000000000..d5c0cf467
--- /dev/null
+++ b/client/vitest.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: "jsdom",
+ globals: true,
+ setupFiles: ["./test/setupTests.ts"],
+ typecheck: {
+ tsconfig: "./tsconfig.test.json",
+ },
+ },
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});