diff --git a/.dockerignore b/.dockerignore index 2af308e..964f3a7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -31,8 +31,8 @@ env/ .gitignore # Documentation -*.md docs/ +*.md # Data and logs data/ @@ -58,7 +58,11 @@ tests/ pytest.ini .pytest_cache/ -# Node (if any frontend) +# Frontend - Exclude development artifacts but allow source +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +frontend/coverage/ node_modules/ package-lock.json yarn.lock diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 8de2b7b..3be2555 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -2,8 +2,6 @@ name: Build and Push Docker Images on: push: - branches: - - main tags: - 'v*' pull_request: diff --git a/config.py b/config.py index c3d5b8c..79b49a3 100644 --- a/config.py +++ b/config.py @@ -10,11 +10,7 @@ class Settings(BaseSettings): # server settings host: str = Field(default="0.0.0.0", description="Host", alias="HOST") port: int = Field(default=8123, description="Port", alias="PORT") - - # Monitoring Settings - enable_monitoring: bool = Field(default=True, description="Enable web-based monitoring interface", alias="ENABLE_MONITORING") - monitoring_path: str = Field(default="/ui", description="Base path for monitoring interface", alias="MONITORING_PATH") - + # Vector Search Settings (using Neo4j built-in vector index) vector_index_name: str = Field(default="knowledge_vectors", description="Neo4j vector index name") vector_dimension: int = Field(default=384, description="Vector embedding dimension") diff --git a/core/app.py b/core/app.py index 12ec36d..b85a97c 100644 --- a/core/app.py +++ b/core/app.py @@ -3,12 +3,13 @@ Responsible for creating and configuring FastAPI application instance """ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.staticfiles import StaticFiles -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, FileResponse from loguru import logger +import os from config import settings from .exception_handlers import setup_exception_handlers @@ -37,47 +38,46 @@ def create_app() -> FastAPI: # set routes setup_routes(app) - - # static file service - app.mount("/static", StaticFiles(directory="static"), name="static") - - # conditionally enable NiceGUI monitoring interface - if settings.enable_monitoring: - try: - from nicegui import ui - from monitoring.task_monitor import setup_monitoring_routes - - # setup NiceGUI monitoring routes - setup_monitoring_routes() - - # integrate NiceGUI with FastAPI - ui.run_with(app, mount_path=settings.monitoring_path) - - logger.info(f"Monitoring interface enabled at {settings.monitoring_path}/monitor") - - except ImportError as e: - logger.warning(f"NiceGUI not available, monitoring interface disabled: {e}") - except Exception as e: - logger.error(f"Failed to setup monitoring interface: {e}") + + # Check if static directory exists (contains built React frontend) + static_dir = "static" + if os.path.exists(static_dir) and os.path.exists(os.path.join(static_dir, "index.html")): + # Mount static assets (JS, CSS, images, etc.) + app.mount("/assets", StaticFiles(directory=os.path.join(static_dir, "assets")), name="assets") + + # SPA fallback - serve index.html for all non-API routes + @app.get("/{full_path:path}") + async def serve_spa(request: Request, full_path: str): + """Serve React SPA with fallback to index.html for client-side routing""" + # API routes are handled by routers, so we only get here for unmatched routes + # Check if this looks like an API call that wasn't found + if full_path.startswith("api/"): + return JSONResponse( + status_code=404, + content={"detail": "Not Found"} + ) + + # For all other routes, serve the React SPA + index_path = os.path.join(static_dir, "index.html") + return FileResponse(index_path) + + logger.info("React frontend enabled - serving SPA from /static") + logger.info("Task monitoring available at /tasks") else: - logger.info("Monitoring interface disabled by configuration") - - # root path - @app.get("/") - async def root(): - """root path interface""" - response_data = { - "message": "Welcome to Code Graph Knowledge Service", - "version": settings.app_version, - "docs": "/docs" if settings.debug else "Documentation disabled in production", - "health": "/api/v1/health" - } - - # conditionally add monitoring URL - if settings.enable_monitoring: - response_data["task_monitor"] = f"{settings.monitoring_path}/monitor" - - return response_data + logger.warning("Static directory not found - React frontend not available") + logger.warning("Run 'cd frontend && npm run build' and copy dist/* to static/") + + # Fallback root endpoint when frontend is not built + @app.get("/") + async def root(): + """root path interface""" + return { + "message": "Welcome to Code Graph Knowledge Service", + "version": settings.app_version, + "docs": "/docs" if settings.debug else "Documentation disabled in production", + "health": "/api/v1/health", + "note": "React frontend not built - see logs for instructions" + } # system information interface @app.get("/info") @@ -89,7 +89,6 @@ async def system_info(): "version": settings.app_version, "python_version": sys.version, "debug_mode": settings.debug, - "monitoring_enabled": settings.enable_monitoring, "services": { "neo4j": { "uri": settings.neo4j_uri, diff --git a/docker/Dockerfile.full b/docker/Dockerfile.full index 04d8962..dccaf9b 100644 --- a/docker/Dockerfile.full +++ b/docker/Dockerfile.full @@ -1,56 +1,88 @@ +# syntax=docker/dockerfile:1.7 # Full Docker image - All features (LLM + Embedding required) -FROM python:3.13-slim as builder + +########### Builder - Build wheels only ########### +FROM python:3.13-slim AS builder ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=1 + PYTHONDONTWRITEBYTECODE=1 -RUN apt-get update && apt-get install -y \ - git \ - curl \ +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ + curl \ + ca-certificates \ + git \ && rm -rf /var/lib/apt/lists/* -RUN pip install uv - WORKDIR /app -# Copy source files needed for package installation -COPY pyproject.toml ./ -COPY api ./api -COPY core ./core -COPY services ./services -COPY monitoring ./monitoring -COPY mcp_tools ./mcp_tools -COPY start.py start_mcp.py mcp_server.py config.py main.py ./ +# Copy requirements file +COPY requirements.txt ./ + +# Build all dependencies as wheels (using cache mount) +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + pip wheel --no-deps -r requirements.txt -w /wheelhouse && \ + pip wheel -r requirements.txt -w /wheelhouse + +########### Frontend Builder - Build React app ########### +FROM node:20-slim AS frontend-builder -# Install the package and its dependencies -RUN uv pip install --system . +WORKDIR /frontend -# ============================================ -# Final stage -# ============================================ -FROM python:3.13-slim +# Copy package files +COPY frontend/package*.json ./ + +# Install dependencies with cache mount +RUN --mount=type=cache,target=/root/.npm \ + npm ci + +# Copy frontend source +COPY frontend/ ./ + +# Build production bundle +RUN npm run build + +########### Runtime - Install wheels + code + frontend ########### +FROM python:3.13-slim AS runtime ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ DEPLOYMENT_MODE=full -RUN apt-get update && apt-get install -y \ - git \ +# Install runtime dependencies only +RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ + ca-certificates \ + git \ && rm -rf /var/lib/apt/lists/* +# Create non-root user RUN useradd -m -u 1000 appuser && \ mkdir -p /app /data /repos && \ chown -R appuser:appuser /app /data /repos WORKDIR /app -COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin +# Copy wheels from builder and install +COPY --from=builder /wheelhouse /wheelhouse +COPY requirements.txt ./ + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + pip install --no-index --find-links=/wheelhouse -r requirements.txt && \ + rm -rf /wheelhouse + +# Copy application code (precise, not everything) +COPY --chown=appuser:appuser api ./api +COPY --chown=appuser:appuser core ./core +COPY --chown=appuser:appuser services ./services +COPY --chown=appuser:appuser mcp_tools ./mcp_tools +COPY --chown=appuser:appuser start.py start_mcp.py mcp_server.py config.py main.py ./ -COPY --chown=appuser:appuser . . +# Copy built frontend from frontend-builder stage +COPY --from=frontend-builder --chown=appuser:appuser /frontend/dist ./static USER appuser diff --git a/docker/Dockerfile.minimal b/docker/Dockerfile.minimal index bc39713..d1b5eb1 100644 --- a/docker/Dockerfile.minimal +++ b/docker/Dockerfile.minimal @@ -1,57 +1,88 @@ +# syntax=docker/dockerfile:1.7 # Minimal Docker image - Code Graph only (No LLM required) -FROM python:3.13-slim as builder + +########### Builder - Build wheels only ########### +FROM python:3.13-slim AS builder ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=1 + PYTHONDONTWRITEBYTECODE=1 -# Install system dependencies -RUN apt-get update && apt-get install -y \ - git \ - curl \ +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ + curl \ + ca-certificates \ + git \ && rm -rf /var/lib/apt/lists/* -RUN pip install uv - WORKDIR /app -# Copy source files needed for package installation -COPY pyproject.toml ./ -COPY api ./api -COPY core ./core -COPY services ./services -COPY monitoring ./monitoring -COPY mcp_tools ./mcp_tools -COPY start.py start_mcp.py mcp_server.py config.py main.py ./ +# Copy requirements file +COPY requirements.txt ./ + +# Build all dependencies as wheels (using cache mount) +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + pip wheel --no-deps -r requirements.txt -w /wheelhouse && \ + pip wheel -r requirements.txt -w /wheelhouse + +########### Frontend Builder - Build React app ########### +FROM node:20-slim AS frontend-builder -# Install the package and its dependencies -RUN uv pip install --system . +WORKDIR /frontend -# ============================================ -# Final stage -# ============================================ -FROM python:3.13-slim +# Copy package files +COPY frontend/package*.json ./ + +# Install dependencies with cache mount +RUN --mount=type=cache,target=/root/.npm \ + npm ci + +# Copy frontend source +COPY frontend/ ./ + +# Build production bundle +RUN npm run build + +########### Runtime - Install wheels + code + frontend ########### +FROM python:3.13-slim AS runtime ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ DEPLOYMENT_MODE=minimal -RUN apt-get update && apt-get install -y \ - git \ +# Install runtime dependencies only +RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ + ca-certificates \ + git \ && rm -rf /var/lib/apt/lists/* +# Create non-root user RUN useradd -m -u 1000 appuser && \ mkdir -p /app /data /repos && \ chown -R appuser:appuser /app /data /repos WORKDIR /app -COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin +# Copy wheels from builder and install +COPY --from=builder /wheelhouse /wheelhouse +COPY requirements.txt ./ + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + pip install --no-index --find-links=/wheelhouse -r requirements.txt && \ + rm -rf /wheelhouse + +# Copy application code (precise, not everything) +COPY --chown=appuser:appuser api ./api +COPY --chown=appuser:appuser core ./core +COPY --chown=appuser:appuser services ./services +COPY --chown=appuser:appuser mcp_tools ./mcp_tools +COPY --chown=appuser:appuser start.py start_mcp.py mcp_server.py config.py main.py ./ -COPY --chown=appuser:appuser . . +# Copy built frontend from frontend-builder stage +COPY --from=frontend-builder --chown=appuser:appuser /frontend/dist ./static USER appuser diff --git a/docker/Dockerfile.standard b/docker/Dockerfile.standard index d7e6ba7..f79e261 100644 --- a/docker/Dockerfile.standard +++ b/docker/Dockerfile.standard @@ -1,56 +1,88 @@ +# syntax=docker/dockerfile:1.7 # Standard Docker image - Code Graph + Memory (Embedding required) -FROM python:3.13-slim as builder + +########### Builder - Build wheels only ########### +FROM python:3.13-slim AS builder ENV PYTHONUNBUFFERED=1 \ - PYTHONDONTWRITEBYTECODE=1 \ - PIP_NO_CACHE_DIR=1 + PYTHONDONTWRITEBYTECODE=1 -RUN apt-get update && apt-get install -y \ - git \ - curl \ +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ + curl \ + ca-certificates \ + git \ && rm -rf /var/lib/apt/lists/* -RUN pip install uv - WORKDIR /app -# Copy source files needed for package installation -COPY pyproject.toml ./ -COPY api ./api -COPY core ./core -COPY services ./services -COPY monitoring ./monitoring -COPY mcp_tools ./mcp_tools -COPY start.py start_mcp.py mcp_server.py config.py main.py ./ +# Copy requirements file +COPY requirements.txt ./ + +# Build all dependencies as wheels (using cache mount) +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + pip wheel --no-deps -r requirements.txt -w /wheelhouse && \ + pip wheel -r requirements.txt -w /wheelhouse + +########### Frontend Builder - Build React app ########### +FROM node:20-slim AS frontend-builder -# Install the package and its dependencies -RUN uv pip install --system . +WORKDIR /frontend -# ============================================ -# Final stage -# ============================================ -FROM python:3.13-slim +# Copy package files +COPY frontend/package*.json ./ + +# Install dependencies with cache mount +RUN --mount=type=cache,target=/root/.npm \ + npm ci + +# Copy frontend source +COPY frontend/ ./ + +# Build production bundle +RUN npm run build + +########### Runtime - Install wheels + code + frontend ########### +FROM python:3.13-slim AS runtime ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ DEPLOYMENT_MODE=standard -RUN apt-get update && apt-get install -y \ - git \ +# Install runtime dependencies only +RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ + ca-certificates \ + git \ && rm -rf /var/lib/apt/lists/* +# Create non-root user RUN useradd -m -u 1000 appuser && \ mkdir -p /app /data /repos && \ chown -R appuser:appuser /app /data /repos WORKDIR /app -COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin +# Copy wheels from builder and install +COPY --from=builder /wheelhouse /wheelhouse +COPY requirements.txt ./ + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + pip install --no-index --find-links=/wheelhouse -r requirements.txt && \ + rm -rf /wheelhouse + +# Copy application code (precise, not everything) +COPY --chown=appuser:appuser api ./api +COPY --chown=appuser:appuser core ./core +COPY --chown=appuser:appuser services ./services +COPY --chown=appuser:appuser mcp_tools ./mcp_tools +COPY --chown=appuser:appuser start.py start_mcp.py mcp_server.py config.py main.py ./ -COPY --chown=appuser:appuser . . +# Copy built frontend from frontend-builder stage +COPY --from=frontend-builder --chown=appuser:appuser /frontend/dist ./static USER appuser diff --git a/env.example b/env.example index 0303589..8ae8f66 100644 --- a/env.example +++ b/env.example @@ -4,10 +4,6 @@ DEBUG=true HOST=0.0.0.0 PORT=8000 -# Monitoring Interface -ENABLE_MONITORING=true -MONITORING_PATH=/ui - # Neo4j Configuration NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j diff --git a/frontend/TEST.md b/frontend/TEST.md new file mode 100644 index 0000000..19c39f8 --- /dev/null +++ b/frontend/TEST.md @@ -0,0 +1,243 @@ +# Frontend Testing Guide + +This project uses **Vitest** for unit and integration testing of React components. + +## Test Stack + +- **Vitest**: Fast unit test framework (Vite-native) +- **React Testing Library**: Component testing utilities +- **@testing-library/jest-dom**: Custom matchers for assertions +- **@testing-library/user-event**: User interaction simulation +- **jsdom**: DOM environment for testing + +## Running Tests + +```bash +# Install dependencies first +npm install + +# Run tests in watch mode (recommended for development) +npm test + +# Run tests once +npm run test + +# Run tests with UI +npm run test:ui + +# Run tests with coverage report +npm run test:coverage +``` + +## Test Structure + +``` +frontend/ +├── src/ +│ ├── components/ +│ │ └── ui/ +│ │ ├── button.tsx +│ │ ├── button.test.tsx # Component tests +│ │ ├── badge.tsx +│ │ ├── badge.test.tsx +│ │ └── ... +│ ├── lib/ +│ │ ├── utils.ts +│ │ ├── utils.test.ts # Utility function tests +│ │ ├── api.ts +│ │ └── api.test.ts # API tests +│ └── test/ +│ └── setup.ts # Test setup file +├── vitest.config.ts # Vitest configuration +└── TEST.md # This file +``` + +## Writing Tests + +### Component Test Example + +```typescript +import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' +import { Button } from './button' +import userEvent from '@testing-library/user-event' + +describe('Button', () => { + it('should render button with text', () => { + render() + expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument() + }) + + it('should handle click events', async () => { + const user = userEvent.setup() + let clicked = false + + render() + await user.click(screen.getByRole('button')) + + expect(clicked).toBe(true) + }) +}) +``` + +### Utility Function Test Example + +```typescript +import { describe, it, expect } from 'vitest' +import { formatBytes } from './utils' + +describe('formatBytes', () => { + it('should format bytes correctly', () => { + expect(formatBytes(0)).toBe('0 Bytes') + expect(formatBytes(1024)).toBe('1 KB') + expect(formatBytes(1024 * 1024)).toBe('1 MB') + }) +}) +``` + +### API Test Example (with mocking) + +```typescript +import { describe, it, expect, vi, beforeEach } from 'vitest' +import axios from 'axios' +import { taskApi } from './api' + +vi.mock('axios') + +describe('taskApi', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should list tasks', async () => { + const mockTasks = { tasks: [], total_count: 0 } + + mockedAxios.create.mockReturnValue({ + get: vi.fn().mockResolvedValue({ data: mockTasks }), + } as any) + + const result = await taskApi.listTasks() + expect(result.data).toEqual(mockTasks) + }) +}) +``` + +## Test Coverage + +Run coverage report to see which parts of the code are tested: + +```bash +npm run test:coverage +``` + +Coverage reports are generated in: +- Terminal: Summary view +- `coverage/index.html`: Detailed HTML report + +### Coverage Targets + +- **Components**: Aim for 80%+ coverage +- **Utils**: Aim for 90%+ coverage +- **API**: Aim for 70%+ coverage (mocked) + +## CI/CD Integration + +Tests can be integrated into CI/CD pipelines: + +```yaml +# .github/workflows/test.yml +- name: Run tests + run: | + cd frontend + npm install + npm run test:coverage +``` + +## Best Practices + +### 1. Test User Behavior, Not Implementation + +✅ **Good**: Test what the user sees and does +```typescript +expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument() +await user.click(screen.getByRole('button')) +``` + +❌ **Bad**: Test internal state or implementation details +```typescript +expect(component.state.isClicked).toBe(true) +``` + +### 2. Use Accessible Queries + +Priority order: +1. `getByRole()` - Best for accessibility +2. `getByLabelText()` - For form inputs +3. `getByPlaceholderText()` - For inputs without labels +4. `getByText()` - For text content +5. `getByTestId()` - Last resort + +### 3. Keep Tests Isolated + +Each test should be independent: +```typescript +beforeEach(() => { + vi.clearAllMocks() // Clear mocks +}) + +afterEach(() => { + cleanup() // Cleanup DOM +}) +``` + +### 4. Mock External Dependencies + +Mock API calls, timers, and third-party libraries: +```typescript +vi.mock('axios') +vi.mock('@tanstack/react-query') +``` + +### 5. Test Error States + +Don't just test the happy path: +```typescript +it('should show error message when API fails', async () => { + mockedApi.listTasks.mockRejectedValue(new Error('Network error')) + + render() + + expect(await screen.findByText(/error/i)).toBeInTheDocument() +}) +``` + +## Debugging Tests + +### 1. Use `screen.debug()` + +```typescript +it('should render component', () => { + render() + screen.debug() // Prints DOM to console +}) +``` + +### 2. Run Single Test + +```bash +npm test -- button.test.tsx +``` + +### 3. Use Vitest UI + +```bash +npm run test:ui +``` + +Opens a browser UI for interactive test debugging. + +## Resources + +- [Vitest Documentation](https://vitest.dev/) +- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) +- [Testing Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) diff --git a/frontend/package.json b/frontend/package.json index c38d6ad..2e43740 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,10 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "test": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest --coverage" }, "dependencies": { "react": "^18.2.0", @@ -37,6 +40,13 @@ "tailwindcss-animate": "^1.0.7", "autoprefixer": "^10.4.19", "postcss": "^8.4.38", - "@tanstack/router-vite-plugin": "^1.58.4" + "@tanstack/router-vite-plugin": "^1.58.4", + "vitest": "^1.6.0", + "@vitest/ui": "^1.6.0", + "@testing-library/react": "^15.0.7", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/user-event": "^14.5.2", + "jsdom": "^24.1.0", + "@vitest/coverage-v8": "^1.6.0" } } diff --git a/frontend/src/components/ui/badge.test.tsx b/frontend/src/components/ui/badge.test.tsx new file mode 100644 index 0000000..aab5877 --- /dev/null +++ b/frontend/src/components/ui/badge.test.tsx @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' +import { Badge } from './badge' + +describe('Badge', () => { + it('should render badge with text', () => { + render(Test Badge) + expect(screen.getByText('Test Badge')).toBeInTheDocument() + }) + + it('should apply default variant', () => { + const { container } = render(Default) + const badge = container.querySelector('.inline-flex') + expect(badge).toHaveClass('bg-primary') + }) + + it('should apply success variant', () => { + const { container } = render(Success) + const badge = container.querySelector('.inline-flex') + expect(badge).toHaveClass('bg-green-600') + }) + + it('should apply destructive variant', () => { + const { container } = render(Error) + const badge = container.querySelector('.inline-flex') + expect(badge).toHaveClass('bg-destructive') + }) + + it('should apply warning variant', () => { + const { container } = render(Warning) + const badge = container.querySelector('.inline-flex') + expect(badge).toHaveClass('bg-yellow-500') + }) + + it('should apply custom className', () => { + const { container } = render(Custom) + const badge = container.querySelector('.inline-flex') + expect(badge).toHaveClass('custom-class') + }) +}) diff --git a/frontend/src/components/ui/button.test.tsx b/frontend/src/components/ui/button.test.tsx new file mode 100644 index 0000000..36f0c9d --- /dev/null +++ b/frontend/src/components/ui/button.test.tsx @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' +import { Button } from './button' +import userEvent from '@testing-library/user-event' + +describe('Button', () => { + it('should render button with text', () => { + render() + expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument() + }) + + it('should handle click events', async () => { + const user = userEvent.setup() + let clicked = false + const handleClick = () => { + clicked = true + } + + render() + await user.click(screen.getByRole('button')) + + expect(clicked).toBe(true) + }) + + it('should be disabled when disabled prop is true', () => { + render() + expect(screen.getByRole('button')).toBeDisabled() + }) + + it('should apply variant classes', () => { + const { container } = render() + const button = container.querySelector('button') + expect(button).toHaveClass('bg-destructive') + }) + + it('should apply size classes', () => { + const { container } = render() + const button = container.querySelector('button') + expect(button).toHaveClass('h-9') + }) +}) diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..ca0aa0b --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,113 @@ +import * as React from 'react' +import { X } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface DialogProps { + open?: boolean + onOpenChange?: (open: boolean) => void + children: React.ReactNode +} + +interface DialogContentProps extends React.HTMLAttributes { + children: React.ReactNode +} + +const Dialog = ({ open, onOpenChange, children }: DialogProps) => { + return ( + <> + {open && ( +
+ {/* Backdrop */} +
onOpenChange?.(false)} + /> + {/* Content */} +
{children}
+
+ )} + + ) +} + +const DialogContent = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( +
+ {children} +
+ ) + } +) +DialogContent.displayName = 'DialogContent' + +interface DialogHeaderProps extends React.HTMLAttributes { + children: React.ReactNode +} + +const DialogHeader = ({ className, children, ...props }: DialogHeaderProps) => { + return ( +
+ {children} +
+ ) +} + +interface DialogTitleProps extends React.HTMLAttributes { + children: React.ReactNode +} + +const DialogTitle = ({ className, children, ...props }: DialogTitleProps) => { + return ( +

+ {children} +

+ ) +} + +interface DialogDescriptionProps extends React.HTMLAttributes { + children: React.ReactNode +} + +const DialogDescription = ({ className, children, ...props }: DialogDescriptionProps) => { + return ( +

+ {children} +

+ ) +} + +interface DialogCloseProps extends React.ButtonHTMLAttributes { + onClose: () => void +} + +const DialogClose = ({ onClose, className, ...props }: DialogCloseProps) => { + return ( + + ) +} + +export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogClose } diff --git a/frontend/src/components/ui/input.test.tsx b/frontend/src/components/ui/input.test.tsx new file mode 100644 index 0000000..3e6d18e --- /dev/null +++ b/frontend/src/components/ui/input.test.tsx @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' +import { Input } from './input' +import userEvent from '@testing-library/user-event' + +describe('Input', () => { + it('should render input field', () => { + render() + expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument() + }) + + it('should handle text input', async () => { + const user = userEvent.setup() + render() + const input = screen.getByRole('textbox') + + await user.type(input, 'Hello') + expect(input).toHaveValue('Hello') + }) + + it('should be disabled when disabled prop is true', () => { + render() + expect(screen.getByRole('textbox')).toBeDisabled() + }) + + it('should accept different input types', () => { + const { container } = render() + const input = container.querySelector('input') + expect(input).toHaveAttribute('type', 'email') + }) + + it('should apply custom className', () => { + const { container } = render() + const input = container.querySelector('input') + expect(input).toHaveClass('custom-input') + }) +}) diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..0b02367 --- /dev/null +++ b/frontend/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = 'Input' + +export { Input } diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..2face72 --- /dev/null +++ b/frontend/src/components/ui/label.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface LabelProps + extends React.LabelHTMLAttributes {} + +const Label = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +