Compare commits

...

2 Commits

Author SHA1 Message Date
Devin AI
bb6ab5a85a style: Format test files with Prettier
Co-Authored-By: ashwin@anthropic.com <ashwin@anthropic.com>
2025-01-16 19:27:49 +00:00
Devin AI
ce7f65b5be Add frontend unit tests
- Set up Vitest with React Testing Library
- Add comprehensive tests for Button and ListPane components
- Configure TypeScript for test environment
- Add test type declarations

Co-Authored-By: ashwin@anthropic.com <ashwin@anthropic.com>
2025-01-16 19:24:51 +00:00
9 changed files with 170 additions and 4 deletions

View File

@@ -18,7 +18,8 @@
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview",
"test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.3", "@modelcontextprotocol/sdk": "^1.0.3",
@@ -40,6 +41,8 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.11.1", "@eslint/js": "^9.11.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.7.5", "@types/node": "^22.7.5",
"@types/react": "^18.3.10", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
@@ -50,10 +53,12 @@
"eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12", "eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.9.0", "globals": "^15.9.0",
"jsdom": "^26.0.0",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"typescript-eslint": "^8.7.0", "typescript-eslint": "^8.7.0",
"vite": "^5.4.8" "vite": "^5.4.8",
"vitest": "^3.0.0"
} }
} }

View File

@@ -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 }) => <span>{item.name}</span>,
title: "Test List",
buttonText: "List Items",
};
it("renders title correctly", () => {
render(<ListPane {...defaultProps} />);
expect(screen.getByText("Test List")).toBeInTheDocument();
});
it("renders list items using renderItem prop", () => {
render(<ListPane {...defaultProps} />);
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
});
it("calls listItems when List Items button is clicked", () => {
render(<ListPane {...defaultProps} />);
fireEvent.click(screen.getByText("List Items"));
expect(defaultProps.listItems).toHaveBeenCalledTimes(1);
});
it("calls clearItems when Clear button is clicked", () => {
render(<ListPane {...defaultProps} />);
fireEvent.click(screen.getByText("Clear"));
expect(defaultProps.clearItems).toHaveBeenCalledTimes(1);
});
it("calls setSelectedItem when an item is clicked", () => {
render(<ListPane {...defaultProps} />);
fireEvent.click(screen.getByText("Item 1"));
expect(defaultProps.setSelectedItem).toHaveBeenCalledWith(
defaultProps.items[0],
);
});
it("disables Clear button when items array is empty", () => {
render(<ListPane {...defaultProps} items={[]} />);
expect(screen.getByText("Clear")).toBeDisabled();
});
it("respects isButtonDisabled prop for List Items button", () => {
render(<ListPane {...defaultProps} isButtonDisabled={true} />);
expect(screen.getByText("List Items")).toBeDisabled();
});
});

View File

@@ -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(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("handles click events", () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText("Click me"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("applies different variants correctly", () => {
const { rerender } = render(<Button variant="default">Default</Button>);
expect(screen.getByText("Default")).toHaveClass("bg-primary");
rerender(<Button variant="outline">Outline</Button>);
expect(screen.getByText("Outline")).toHaveClass("border-input");
rerender(<Button variant="secondary">Secondary</Button>);
expect(screen.getByText("Secondary")).toHaveClass("bg-secondary");
});
it("applies different sizes correctly", () => {
const { rerender } = render(<Button size="default">Default</Button>);
expect(screen.getByText("Default")).toHaveClass("h-9");
rerender(<Button size="sm">Small</Button>);
expect(screen.getByText("Small")).toHaveClass("h-8");
rerender(<Button size="lg">Large</Button>);
expect(screen.getByText("Large")).toHaveClass("h-10");
});
it("forwards ref correctly", () => {
const ref = createRef<HTMLButtonElement>();
render(<Button ref={ref}>Button with ref</Button>);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("renders as a different element when asChild is true", () => {
render(
<Button asChild>
<a href="#">Link Button</a>
</Button>,
);
expect(screen.getByText("Link Button").tagName).toBe("A");
});
});

12
client/src/test.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/// <reference types="vitest/globals" />
/// <reference types="@testing-library/jest-dom" />
import "@testing-library/jest-dom";
declare global {
namespace Vi {
interface JestAssertion<T = any> extends jest.Matchers<void, T> {}
}
}
export {};

View File

@@ -0,0 +1,6 @@
/// <reference types="vitest/globals" />
/// <reference types="@testing-library/jest-dom" />
import "@testing-library/jest-dom/vitest";
// Add any additional test setup, custom matchers, or global mocks here
// This file runs before each test file

View File

@@ -4,6 +4,7 @@
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"types": ["vitest/globals", "@testing-library/jest-dom"],
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
@@ -26,5 +27,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["src"] "include": ["src", "test"]
} }

View File

@@ -2,7 +2,8 @@
"files": [], "files": [],
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" } { "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.test.json" }
], ],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",

View File

@@ -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"]
}

20
client/vitest.config.ts Normal file
View File

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