diff --git a/package.json b/package.json index 89d9182..cdb2d1f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,10 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/jsdom": "^21.1.7", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "eslint": "^10.2.1", @@ -40,6 +44,7 @@ "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", "husky": "^9.1.7", + "jsdom": "^26.0.0", "lint-staged": "^16.4.0", "prettier": "^3.8.3", "typescript-eslint": "^8.58.2", diff --git a/src/components/terminal/TerminalInput.test.tsx b/src/components/terminal/TerminalInput.test.tsx index e69de29..7fa17b1 100644 --- a/src/components/terminal/TerminalInput.test.tsx +++ b/src/components/terminal/TerminalInput.test.tsx @@ -0,0 +1,40 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { TerminalInput } from './TerminalInput'; +import { describe, it, expect, vi } from 'vitest'; +import React from 'react'; + +describe('TerminalInput component', () => { + it('renders correctly', () => { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render(); + expect(screen.getByPlaceholderText('Type here...')).toBeInTheDocument(); + }); + + it('calls onChange when typing', () => { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render(); + const input = screen.getByPlaceholderText('Type here...'); + fireEvent.change(input, { target: { value: 'hello' } }); + expect(onChange).toHaveBeenCalledWith('hello'); + }); + + it('calls onSubmit when Enter is pressed', () => { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render(); + const input = screen.getByPlaceholderText('Type here...'); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + expect(onSubmit).toHaveBeenCalled(); + }); + + it('clears input when Escape is pressed', () => { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render(); + const input = screen.getByPlaceholderText('Type here...'); + fireEvent.keyDown(input, { key: 'Escape', code: 'Escape' }); + expect(onChange).toHaveBeenCalledWith(''); + }); +}); diff --git a/src/components/ui/Button.test.tsx b/src/components/ui/Button.test.tsx new file mode 100644 index 0000000..f536318 --- /dev/null +++ b/src/components/ui/Button.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from '@testing-library/react'; +import { Button } from './Button'; +import { describe, it, expect } from 'vitest'; + +describe('Button component', () => { + it('renders children correctly', () => { + render(); + expect(screen.getByText('Click Me')).toBeInTheDocument(); + }); + + it('shows loading state when loading prop is true', () => { + render(); + expect(screen.getByText('⏳')).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); diff --git a/src/components/ui/Input.test.tsx b/src/components/ui/Input.test.tsx new file mode 100644 index 0000000..9447760 --- /dev/null +++ b/src/components/ui/Input.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/react'; +import { Input } from './Input'; +import { describe, it, expect } from 'vitest'; +import React from 'react'; + +describe('Input component', () => { + it('renders correctly', () => { + render(); + expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument(); + }); + + it('passes ref correctly', () => { + const ref = React.createRef(); + render(); + expect(ref.current).toBeInstanceOf(HTMLInputElement); + }); + + it('applies error styles when hasError is true', () => { + render(); + const input = screen.getByTestId('error-input'); + expect(input.style.border).toContain('var(--status-error)'); + }); +}); diff --git a/src/components/ui/MarkdownRenderer.test.tsx b/src/components/ui/MarkdownRenderer.test.tsx new file mode 100644 index 0000000..dc2469e --- /dev/null +++ b/src/components/ui/MarkdownRenderer.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; +import { MarkdownRenderer } from './MarkdownRenderer'; +import { describe, it, expect } from 'vitest'; +import React from 'react'; + +describe('MarkdownRenderer component', () => { + it('renders markdown content correctly', () => { + render(); + // Testing specific output depends on react-markdown, we just test if the component renders the text + expect(screen.getByText('Hello World')).toBeInTheDocument(); + }); + + it('renders code blocks correctly', () => { + render(); + expect(screen.getByText('inline code')).toBeInTheDocument(); + expect(screen.getByText('inline code').tagName).toBe('CODE'); + }); + + it('renders links correctly', () => { + render(); + const link = screen.getByRole('link', { name: 'GitHub' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://github.com'); + }); +}); diff --git a/src/components/ui/Panel.test.tsx b/src/components/ui/Panel.test.tsx new file mode 100644 index 0000000..9ac0af9 --- /dev/null +++ b/src/components/ui/Panel.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; +import { Panel } from './Panel'; +import { describe, it, expect } from 'vitest'; +import React from 'react'; + +describe('Panel component', () => { + it('renders children correctly', () => { + render(Panel Content); + expect(screen.getByText('Panel Content')).toBeInTheDocument(); + }); + + it('renders title when provided', () => { + render(Content); + expect(screen.getByText('[ Test Title ]')).toBeInTheDocument(); + }); + + it('renders headerRight when provided', () => { + render( + Action}> + Content + , + ); + expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument(); + }); +}); diff --git a/src/components/ui/ProgressBar.test.tsx b/src/components/ui/ProgressBar.test.tsx new file mode 100644 index 0000000..824f1be --- /dev/null +++ b/src/components/ui/ProgressBar.test.tsx @@ -0,0 +1,35 @@ +import { render, screen } from '@testing-library/react'; +import { ProgressBar } from './ProgressBar'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import React from 'react'; + +describe('ProgressBar component', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('renders correctly with current and total', () => { + render(); + expect(screen.getByText(/5\/10 \(50%\)/)).toBeInTheDocument(); + }); + + it('renders label when provided', () => { + render(); + expect(screen.getByText('Processing')).toBeInTheDocument(); + }); + + it('hides count when showCount is false', () => { + render(); + expect(screen.queryByText(/5\/10 \(50%\)/)).not.toBeInTheDocument(); + }); + + it('shows ETA when startTime is provided', () => { + const startTime = Date.now(); + render(); + expect(screen.getByText(/ETA:/)).toBeInTheDocument(); + }); +}); diff --git a/src/components/ui/ScrollArea.test.tsx b/src/components/ui/ScrollArea.test.tsx new file mode 100644 index 0000000..8079ef4 --- /dev/null +++ b/src/components/ui/ScrollArea.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/react'; +import { ScrollArea } from './ScrollArea'; +import { describe, it, expect } from 'vitest'; +import React from 'react'; + +describe('ScrollArea component', () => { + it('renders children correctly', () => { + render(Scrollable Content); + expect(screen.getByText('Scrollable Content')).toBeInTheDocument(); + }); + + it('applies maxHeight when provided', () => { + render(Scroll Area Content); + const element = screen.getByText('Scroll Area Content'); + expect(element.style.maxHeight).toBe('200px'); + }); + + it('passes innerRef correctly', () => { + const ref = React.createRef(); + render(Content); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/vite-env.d.ts b/vite-env.d.ts index e92b4bd..6560e54 100644 --- a/vite-env.d.ts +++ b/vite-env.d.ts @@ -1,4 +1,5 @@ /// +/// interface ImportMetaEnv { readonly VITE_GITHUB_TOKEN: string; diff --git a/vite.config.ts b/vite.config.ts index 0e77097..80a6bac 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import path from 'path'; @@ -14,4 +14,9 @@ export default defineConfig({ port: 5173, open: true, }, + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + globals: true, + }, });